diff --git a/backend/Squidex.sln b/backend/Squidex.sln
index 1215fb2ef..9abce5569 100644
--- a/backend/Squidex.sln
+++ b/backend/Squidex.sln
@@ -30,12 +30,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Users.MongoD
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Users.Tests", "tests\Squidex.Domain.Users.Tests\Squidex.Domain.Users.Tests.csproj", "{42184546-E3CB-4D4F-9495-43979B9C63B9}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Infrastructure.GetEventStore", "src\Squidex.Infrastructure.GetEventStore\Squidex.Infrastructure.GetEventStore.csproj", "{EF75E488-1324-4E18-A1BD-D3A05AE67B1F}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Infrastructure.Azure", "src\Squidex.Infrastructure.Azure\Squidex.Infrastructure.Azure.csproj", "{7931187E-A1E6-4F89-8BC8-20A1E445579F}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{360C300D-0F7E-439D-A437-714C959E3CAD}"
ProjectSection(SolutionItems) = preProject
+ Squidex.ruleset = Squidex.ruleset
stylecop.json = stylecop.json
EndProjectSection
EndProject
@@ -179,30 +176,6 @@ Global
{42184546-E3CB-4D4F-9495-43979B9C63B9}.Release|x64.Build.0 = Release|Any CPU
{42184546-E3CB-4D4F-9495-43979B9C63B9}.Release|x86.ActiveCfg = Release|Any CPU
{42184546-E3CB-4D4F-9495-43979B9C63B9}.Release|x86.Build.0 = Release|Any CPU
- {EF75E488-1324-4E18-A1BD-D3A05AE67B1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {EF75E488-1324-4E18-A1BD-D3A05AE67B1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {EF75E488-1324-4E18-A1BD-D3A05AE67B1F}.Debug|x64.ActiveCfg = Debug|Any CPU
- {EF75E488-1324-4E18-A1BD-D3A05AE67B1F}.Debug|x64.Build.0 = Debug|Any CPU
- {EF75E488-1324-4E18-A1BD-D3A05AE67B1F}.Debug|x86.ActiveCfg = Debug|Any CPU
- {EF75E488-1324-4E18-A1BD-D3A05AE67B1F}.Debug|x86.Build.0 = Debug|Any CPU
- {EF75E488-1324-4E18-A1BD-D3A05AE67B1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {EF75E488-1324-4E18-A1BD-D3A05AE67B1F}.Release|Any CPU.Build.0 = Release|Any CPU
- {EF75E488-1324-4E18-A1BD-D3A05AE67B1F}.Release|x64.ActiveCfg = Release|Any CPU
- {EF75E488-1324-4E18-A1BD-D3A05AE67B1F}.Release|x64.Build.0 = Release|Any CPU
- {EF75E488-1324-4E18-A1BD-D3A05AE67B1F}.Release|x86.ActiveCfg = Release|Any CPU
- {EF75E488-1324-4E18-A1BD-D3A05AE67B1F}.Release|x86.Build.0 = Release|Any CPU
- {7931187E-A1E6-4F89-8BC8-20A1E445579F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7931187E-A1E6-4F89-8BC8-20A1E445579F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7931187E-A1E6-4F89-8BC8-20A1E445579F}.Debug|x64.ActiveCfg = Debug|Any CPU
- {7931187E-A1E6-4F89-8BC8-20A1E445579F}.Debug|x64.Build.0 = Debug|Any CPU
- {7931187E-A1E6-4F89-8BC8-20A1E445579F}.Debug|x86.ActiveCfg = Debug|Any CPU
- {7931187E-A1E6-4F89-8BC8-20A1E445579F}.Debug|x86.Build.0 = Debug|Any CPU
- {7931187E-A1E6-4F89-8BC8-20A1E445579F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7931187E-A1E6-4F89-8BC8-20A1E445579F}.Release|Any CPU.Build.0 = Release|Any CPU
- {7931187E-A1E6-4F89-8BC8-20A1E445579F}.Release|x64.ActiveCfg = Release|Any CPU
- {7931187E-A1E6-4F89-8BC8-20A1E445579F}.Release|x64.Build.0 = Release|Any CPU
- {7931187E-A1E6-4F89-8BC8-20A1E445579F}.Release|x86.ActiveCfg = Release|Any CPU
- {7931187E-A1E6-4F89-8BC8-20A1E445579F}.Release|x86.Build.0 = Release|Any CPU
{F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -326,8 +299,6 @@ Global
{F7771E22-47BD-45C4-A133-FD7F1DE27CA0} = {7EDE8CF1-B1E4-4005-B154-834B944E0D7A}
{27CF800D-890F-4882-BF05-44EC3233537D} = {7EDE8CF1-B1E4-4005-B154-834B944E0D7A}
{42184546-E3CB-4D4F-9495-43979B9C63B9} = {7EDE8CF1-B1E4-4005-B154-834B944E0D7A}
- {EF75E488-1324-4E18-A1BD-D3A05AE67B1F} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
- {7931187E-A1E6-4F89-8BC8-20A1E445579F} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
{F0A83301-50A5-40EA-A1A2-07C7858F5A3F} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A}
{6B3F75B6-5888-468E-BA4F-4FC725DAEF31} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A}
{79FEF326-CA5E-4698-B2BA-C16A4580B4D5} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A}
diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json
index 83d250a89..7daa8257a 100644
--- a/backend/i18n/frontend_en.json
+++ b/backend/i18n/frontend_en.json
@@ -41,9 +41,11 @@
"apps.leaveFailed": "Failed to leave app. Please reload.",
"apps.listPageTitle": "Apps",
"apps.loadFailed": "Failed to load apps. Please reload.",
+ "apps.loadSettingsFailed": "Failed to update UI settings. Please reload.",
"apps.removeImage": "Remove image",
"apps.removeImageFailed": "Failed to remove app image. Please reload.",
"apps.updateFailed": "Failed to update app. Please reload.",
+ "apps.updateSettingsFailed": "Faield to update UI settings. Please reload.",
"apps.upgradeHintCurrent": "You are on the {plan} plan.",
"apps.upgradeHintUpgrade": "Upgrade!",
"apps.uploadImage": "Drop an file to replace the app image. Use a square size.",
@@ -52,11 +54,18 @@
"apps.uploadImageTooBig": "App image is too big.",
"apps.welcomeSubtitle": "Welcome to Squidex.",
"apps.welcomeTitle": "Hi {user}",
+ "appSettings.editors.deleteConfirmText": "Do you really want to remove this Editor URL?",
+ "appSettings.editors.deleteConfirmTitle": "Delete Editor URL",
+ "appSettings.editors.empty": "No Editor URL created yet.",
+ "appSettings.editors.title": "Custom Editors",
"appSettings.hideScheduler": "Hide dialog for scheduled publishing",
+ "appSettings.patterns.deleteConfirmText": "Do you really want to remove this pattern?",
+ "appSettings.patterns.deleteConfirmTitle": "Delete pattern",
+ "appSettings.patterns.empty": "No pattern created yet.",
+ "appSettings.patterns.title": "Patterns",
"appSettings.refreshTooltip": "Refresh UI Settings (CTRL + SHIFT + R)",
"appSettings.reloaded": "UI Settings reloaded.",
"appSettings.title": "UI Settings",
- "appSettings.updateFailed": "Failed to update UI settings. Please reload.",
"assets.createFolder": "Create Folder",
"assets.createFolderFailed": "Failed to create asset folder. Please reload.",
"assets.createFolderTooltip": "Create new folder (CTRL + SHIFT + G)",
@@ -331,6 +340,7 @@
"common.settings": "Settings",
"common.sidebar": "Sidebar Extension",
"common.sidebarTour": "The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time.",
+ "common.skipped": "Skipped",
"common.slug": "Slug",
"common.stars.max": "Must not have more more than 15 stars",
"common.status": "Status",
@@ -528,10 +538,6 @@
"dashboard.trafficSummaryCard": "API Traffic Summary",
"dashboard.welcomeText": "Welcome to **{app}** dashboard.",
"dashboard.welcomeTitle": "Hi {user}",
- "editors.deleteConfirmText": "Do you really want to remove this Editor URL?",
- "editors.deleteConfirmTitle": "Delete Editor URL",
- "editors.empty": "No Editor URL created yet.",
- "editors.title": "Custom Editors",
"eventConsumers.count": "Count",
"eventConsumers.loadFailed": "Failed to load event consumers. Please reload.",
"eventConsumers.pageTitle": "Event Consumers",
@@ -563,10 +569,6 @@
"news.headline": "What's new?",
"news.title": "New Features",
"notifo.subscripeTooltip": "Click this button to subscribe to all changes and to receive push notifications.",
- "patterns.deleteConfirmText": "Do you really want to remove this pattern?",
- "patterns.deleteConfirmTitle": "Delete pattern",
- "patterns.empty": "No pattern created yet.",
- "patterns.title": "Patterns",
"plans.billingPortal": "Billing Portal",
"plans.billingPortalHint": "Go to Billing Portal for payment history and subscription overview.",
"plans.change": "Change",
@@ -590,7 +592,7 @@
"roles.addFailed": "Failed to add role. Please reload.",
"roles.default.owner": "Can do everything, including deleting the app.",
"roles.default.reader": "Can only read assets and contents.",
- "roles.defaults.developer": "Can use the API view, edit assets, contents, schemas, rules, workflows and patterns.",
+ "roles.defaults.developer": "Can use the API view, edit assets, contents, schemas, rules, workflows and appSettings.patterns.",
"roles.defaults.editor": "Can edit assets and contents and view workflows.",
"roles.deleteConfirmText": "Delete role",
"roles.deleteConfirmTitle": "Do you really want to delete the role?",
@@ -611,7 +613,8 @@
"roles.revokeFailed": "Failed to revoke role. Please reload.",
"roles.roleNamePlaceholder": "Enter role name",
"roles.updateFailed": "Failed to update role. Please reload.",
- "rules.actionEdit": "Edit Action",
+ "rules.actionData": "Action Data",
+ "rules.actionHint": "The selection of the action type cannot be changed later.",
"rules.cancelFailed": "Failed to cancel rule. Please reload.",
"rules.create": "New Rule",
"rules.createFailed": "Failed to create rule. Please reload.",
@@ -619,10 +622,8 @@
"rules.deleteConfirmText": "Do you really want to delete the rule?",
"rules.deleteConfirmTitle": "Delete rule",
"rules.deleteFailed": "Failed to delete rule. Please reload.",
- "rules.disableFailed": "Failed to disable rule. Please reload.",
"rules.empty": "No rule created yet.",
"rules.emptyAddRule": "Add Rule",
- "rules.enableFailed": "Failed to enable rule. Please reload.",
"rules.enqueued": "Rule has been added to the queue.",
"rules.itemPageTitle": "Rule",
"rules.listPageTitle": "Rules",
@@ -642,6 +643,7 @@
"rules.ruleEvents.nextAttemptLabel": "Next",
"rules.ruleEvents.numAttemptsLabel": "Attempts",
"rules.ruleEvents.reloaded": "RuleEvents reloaded.",
+ "rules.ruleSimulator.listPageTitle": "Simulator",
"rules.ruleSyntax.if": "If",
"rules.ruleSyntax.then": "then",
"rules.run": "Run",
@@ -650,17 +652,16 @@
"rules.runningRule": "Rule '{name}' is currently running.",
"rules.runRuleConfirmText": "Do you really want to run the rule for all events?",
"rules.runRuleConfirmTitle": "Run rule",
+ "rules.simulate": "Simulate",
+ "rules.simulateTooltip": "Simulate this rules using the last 100 events.",
+ "rules.simulator": "Simulator",
"rules.stop": "Rule will stop soon.",
"rules.triggerConfirmText": "Do you really want to trigger the rule?",
"rules.triggerConfirmTitle": "Trigger rule",
- "rules.triggerEdit": "Edit Trigger",
"rules.triggerFailed": "Failed to trigger rule. Please reload.",
+ "rules.triggerHint": "The selection of the trigger type cannot be changed later.",
"rules.unnamed": "Unnamed Rule",
"rules.updateFailed": "Failed to update rule. Please reload.",
- "rules.wizard.actionHint": "The selection of the action type cannot be changed later.",
- "rules.wizard.selectAction": "Select Action",
- "rules.wizard.selectTrigger": "Select Trigger",
- "rules.wizard.triggerHint": "The selection of the trigger type cannot be changed later.",
"schemas.addField": "Add Field",
"schemas.addFieldAndClose": "Create and close",
"schemas.addFieldAndCreate": "Create and add field",
@@ -883,7 +884,7 @@
"tour.step1Next": "Continue",
"tour.step1Text": "An App is the repository for your project, e.g. (blog, web shop or mobile app). You can assign contributors to your app to work together.\n\nYou can create an unlimited number of Apps in Squidex to manage multiple projects at the same time.",
"tour.step2Next": "Keep going!",
- "tour.step2Text": "Schemas define the structure of your content, the fields and the data types of a content item.\n\nBefore you can add content to your schema, make sure to hit the 'Publish' button at the top to make the schema available to your content editors.",
+ "tour.step2Text": "Schemas define the structure of your content, the fields and the data types of a content item.\n\nBefore you can add content to your schema, make sure to hit the 'Publish' button at the top to make the schema available to your content appSettings.editors.",
"tour.step3Next": "Almost there!",
"tour.step3Text": "Content is the actual data in your app which is grouped by the schema.\n\nSelect a published schema first, then add content for this schema.",
"tour.step4Next": "Got It!",
diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json
index 7c7f2d19f..510a7e18c 100644
--- a/backend/i18n/frontend_it.json
+++ b/backend/i18n/frontend_it.json
@@ -41,9 +41,11 @@
"apps.leaveFailed": "Non è stato possibile uscire dall'app. Per favore ricarica.",
"apps.listPageTitle": "App",
"apps.loadFailed": "Non è stato possibile caricare le App. Per favore ricarica.",
+ "apps.loadSettingsFailed": "Failed to update UI settings. Please reload.",
"apps.removeImage": "Rimuovi l'immagine",
"apps.removeImageFailed": "Non è stato possibile rimuovere l'immagine dell'app. Per favore ricarica.",
"apps.updateFailed": "Non è stato possibile aggiornare l'app. Per favore ricarica.",
+ "apps.updateSettingsFailed": "Faield to update UI settings. Please reload.",
"apps.upgradeHintCurrent": "Tu sei nel piano {plan}.",
"apps.upgradeHintUpgrade": "Aggiorna!",
"apps.uploadImage": "Trascina il file per sostituire l'immagine dell'app. Utilizza una dimensione quadrata.",
@@ -52,11 +54,18 @@
"apps.uploadImageTooBig": "L'immagine dell'app è troppo grande.",
"apps.welcomeSubtitle": "Benvenuto su Squidex.",
"apps.welcomeTitle": "Ciao {user}",
+ "appSettings.editors.deleteConfirmText": "Do you really want to remove this Editor URL?",
+ "appSettings.editors.deleteConfirmTitle": "Delete Editor URL",
+ "appSettings.editors.empty": "No Editor URL created yet.",
+ "appSettings.editors.title": "Custom Editors",
"appSettings.hideScheduler": "Hide dialog for scheduled publishing",
+ "appSettings.patterns.deleteConfirmText": "Sei sicuro di voler rimuovere il pattern?",
+ "appSettings.patterns.deleteConfirmTitle": "Cancella il pattern",
+ "appSettings.patterns.empty": "Nessun pattern è stato ancora creato.",
+ "appSettings.patterns.title": "Patterns",
"appSettings.refreshTooltip": "Refresh UI Settings (CTRL + SHIFT + R)",
"appSettings.reloaded": "UI Settings reloaded.",
"appSettings.title": "UI Settings",
- "appSettings.updateFailed": "Failed to update UI settings. Please reload.",
"assets.createFolder": "Crea cartella",
"assets.createFolderFailed": "Non è stato possibile creare la cartella degli asset. Per favore ricarica.",
"assets.createFolderTooltip": "Crea una nuova cartella (CTRL + SHIFT + G)",
@@ -331,6 +340,7 @@
"common.settings": "Impostazioni",
"common.sidebar": "Estensione della barra di navigazione laterale",
"common.sidebarTour": "La barra di navigazione laterale contiene specifici utili collegamenti per il contesto. Qui puoi visualizzare la cronologia dei cambiamenti di questo schema.",
+ "common.skipped": "Skipped",
"common.slug": "Slug",
"common.stars.max": "Non deve avere più di 15 stelle",
"common.status": "Stato",
@@ -528,10 +538,6 @@
"dashboard.trafficSummaryCard": "Riepilogo del traffico delle API",
"dashboard.welcomeText": "Benvenuto sulla dashboard **{app}**.",
"dashboard.welcomeTitle": "Ciao {user}",
- "editors.deleteConfirmText": "Do you really want to remove this Editor URL?",
- "editors.deleteConfirmTitle": "Delete Editor URL",
- "editors.empty": "No Editor URL created yet.",
- "editors.title": "Custom Editors",
"eventConsumers.count": "Conteggio",
"eventConsumers.loadFailed": "Non è stato possibile caricare event consumers. Per favore ricarica.",
"eventConsumers.pageTitle": "Eventi degli utenti",
@@ -563,10 +569,6 @@
"news.headline": "Che cosa c'è di nuovo?",
"news.title": "Nuove funzionalità",
"notifo.subscripeTooltip": "Fai clic su questo pulsante per iscriverti a tutte le modifiche e ricevere le notifiche push.",
- "patterns.deleteConfirmText": "Sei sicuro di voler rimuovere il pattern?",
- "patterns.deleteConfirmTitle": "Cancella il pattern",
- "patterns.empty": "Nessun pattern è stato ancora creato.",
- "patterns.title": "Patterns",
"plans.billingPortal": "Portale di fatturazione",
"plans.billingPortalHint": "Vai al portale di fatturazione per lo storico dei pagamenti e una panoramica per l'abbonamento.",
"plans.change": "Cambia",
@@ -611,7 +613,8 @@
"roles.revokeFailed": "Non è stato possibile rimuovere il ruolo. Per favore ricarica.",
"roles.roleNamePlaceholder": "Inserisci il nome del ruolo",
"roles.updateFailed": "Non è stato possibile aggiornare il ruolo. Per favore ricarica.",
- "rules.actionEdit": "Modifica l'Azione",
+ "rules.actionData": "Action Data",
+ "rules.actionHint": "The selection of the action type cannot be changed later.",
"rules.cancelFailed": "Non è stato possibile eliminare la regola. Per favore ricarica.",
"rules.create": "Crea un nuova Regola",
"rules.createFailed": "Non è stato possibile creare una nuova regola. Per favore ricarica.",
@@ -619,10 +622,8 @@
"rules.deleteConfirmText": "Sei sicuro di voler eliminare la regola?",
"rules.deleteConfirmTitle": "Cancella la regola",
"rules.deleteFailed": "Non è stato possibile eliminare la regola. Per favore ricarica.",
- "rules.disableFailed": "Non è stato possibile disabilitare la regola. Per favore ricarica.",
"rules.empty": "Nessuna regola è stato ancora creata.",
"rules.emptyAddRule": "Aggiungi una regola",
- "rules.enableFailed": "Non è stato possibile abilitare la regola. Per favore ricarica.",
"rules.enqueued": "La regola è stata aggiunta alle code.",
"rules.itemPageTitle": "Rule",
"rules.listPageTitle": "Regole",
@@ -642,6 +643,7 @@
"rules.ruleEvents.nextAttemptLabel": "Successivo",
"rules.ruleEvents.numAttemptsLabel": "Tentativi",
"rules.ruleEvents.reloaded": "Eventi della regola ricaricati.",
+ "rules.ruleSimulator.listPageTitle": "Simulator",
"rules.ruleSyntax.if": "Se",
"rules.ruleSyntax.then": "Allora",
"rules.run": "Esegui",
@@ -650,17 +652,16 @@
"rules.runningRule": "La regola '{name}' è attualmente in esecuzione.",
"rules.runRuleConfirmText": "Sei sicuro di voler eseguire la regola per tutti gli eventi?",
"rules.runRuleConfirmTitle": "Esegui la regola",
+ "rules.simulate": "Simulate",
+ "rules.simulateTooltip": "Simulate this rules using the last 100 events.",
+ "rules.simulator": "Simulator",
"rules.stop": "La regola si fermerà al più presto.",
"rules.triggerConfirmText": "Sei sicuro che voler attivare la regola?",
"rules.triggerConfirmTitle": "Attiva la regola",
- "rules.triggerEdit": "Modifica l'Attivazione",
"rules.triggerFailed": "Non è stato possibile attivare la regola. Per favore ricarica.",
+ "rules.triggerHint": "The selection of the trigger type cannot be changed later.",
"rules.unnamed": "Regola senza nome",
"rules.updateFailed": "Non è stato possibile aggiornare la regola. Per favore ricarica.",
- "rules.wizard.actionHint": "La selezione del tipo di azione non potrà essere modificata successivamente.",
- "rules.wizard.selectAction": "Seleziona l'Azione",
- "rules.wizard.selectTrigger": "Seleziona l'Attivazione",
- "rules.wizard.triggerHint": "La selezione del tipo di attivazione non potrà essere modificata successivamente.",
"schemas.addField": "Aggiungi un Campo",
"schemas.addFieldAndClose": "Crea e chiudi",
"schemas.addFieldAndCreate": "Crea e aggiungi il campo",
diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json
index 572b47cc4..40fa88482 100644
--- a/backend/i18n/frontend_nl.json
+++ b/backend/i18n/frontend_nl.json
@@ -41,9 +41,11 @@
"apps.leaveFailed": "Failed to leave app. Please reload.",
"apps.listPageTitle": "Apps",
"apps.loadFailed": "Laden van apps is mislukt. Laad opnieuw.",
+ "apps.loadSettingsFailed": "Failed to update UI settings. Please reload.",
"apps.removeImage": "Afbeelding verwijderen",
"apps.removeImageFailed": "Verwijderen van app-afbeelding is mislukt. Laad opnieuw.",
"apps.updateFailed": "Update app mislukt. Laad opnieuw.",
+ "apps.updateSettingsFailed": "Faield to update UI settings. Please reload.",
"apps.upgradeHintCurrent": "Je zit in het plan {plan}.",
"apps.upgradeHintUpgrade": "Upgrade!",
"apps.uploadImage": "Zet een bestand neer om de app-afbeelding te vervangen. Gebruik een vierkant formaat.",
@@ -52,11 +54,18 @@
"apps.uploadImageTooBig": "App-afbeelding is te groot.",
"apps.welcomeSubtitle": "Welkom bij Squidex.",
"apps.welcomeTitle": "Hallo {user}",
+ "appSettings.editors.deleteConfirmText": "Do you really want to remove this Editor URL?",
+ "appSettings.editors.deleteConfirmTitle": "Delete Editor URL",
+ "appSettings.editors.empty": "No Editor URL created yet.",
+ "appSettings.editors.title": "Custom Editors",
"appSettings.hideScheduler": "Hide dialog for scheduled publishing",
+ "appSettings.patterns.deleteConfirmText": "Wil je dit patroon echt verwijderen?",
+ "appSettings.patterns.deleteConfirmTitle": "Verwijder patroon",
+ "appSettings.patterns.empty": "Nog geen patroon gemaakt.",
+ "appSettings.patterns.title": "Patterns",
"appSettings.refreshTooltip": "Refresh UI Settings (CTRL + SHIFT + R)",
"appSettings.reloaded": "UI Settings reloaded.",
"appSettings.title": "UI Settings",
- "appSettings.updateFailed": "Failed to update UI settings. Please reload.",
"assets.createFolder": "Map maken",
"assets.createFolderFailed": "Maken van een map is mislukt. Laad opnieuw.",
"assets.createFolderTooltip": "Nieuwe map maken (CTRL + SHIFT + G)",
@@ -331,6 +340,7 @@
"common.settings": "Instellingen",
"common.sidebar": "Zijbalk Uitbreiding",
"common.sidebarTour": "De zijbalknavigatie bevat nuttige contextspecifieke links. Hier kun je de geschiedenis bekijken hoe dit schema in de loop van de tijd is veranderd.",
+ "common.skipped": "Skipped",
"common.slug": "Slug",
"common.stars.max": "Mag niet meer dan 15 sterren hebben",
"common.status": "Status",
@@ -528,10 +538,6 @@
"dashboard.trafficSummaryCard": "API Verkeer Samenvatting",
"dashboard.welcomeText": "Welkom bij **{app}** dashboard.",
"dashboard.welcomeTitle": "Hallo {user}",
- "editors.deleteConfirmText": "Do you really want to remove this Editor URL?",
- "editors.deleteConfirmTitle": "Delete Editor URL",
- "editors.empty": "No Editor URL created yet.",
- "editors.title": "Custom Editors",
"eventConsumers.count": "Tellen",
"eventConsumers.loadFailed": "Kan gebeurtenisgebruikers niet laden. Laad opnieuw.",
"eventConsumers.pageTitle": "Evenementconsumenten",
@@ -563,10 +569,6 @@
"news.headline": "Wat is er nieuw?",
"news.title": "Nieuwe functies",
"notifo.subscripeTooltip": "Klik op deze knop om je te abonneren op alle wijzigingen en om pushmeldingen te ontvangen.",
- "patterns.deleteConfirmText": "Wil je dit patroon echt verwijderen?",
- "patterns.deleteConfirmTitle": "Verwijder patroon",
- "patterns.empty": "Nog geen patroon gemaakt.",
- "patterns.title": "Patterns",
"plans.billingPortal": "Factureringsportal",
"plans.billingPortalHint": "Ga naar het factureringsportaal voor betalingsgeschiedenis en abonnementsoverzicht.",
"plans.change": "Wijzigen",
@@ -611,7 +613,8 @@
"roles.revokeFailed": "Kan rol niet intrekken. Laad opnieuw.",
"roles.roleNamePlaceholder": "Voer de rolnaam in",
"roles.updateFailed": "Update rol mislukt. Laad opnieuw.",
- "rules.actionEdit": "Bewerk actie",
+ "rules.actionData": "Action Data",
+ "rules.actionHint": "The selection of the action type cannot be changed later.",
"rules.cancelFailed": "Annuleren van regel is mislukt. Laad opnieuw.",
"rules.create": "Maak een nieuwe regel",
"rules.createFailed": "Maken van regel is mislukt. Laad opnieuw.",
@@ -619,10 +622,8 @@
"rules.deleteConfirmText": "Wil je de regel echt verwijderen?",
"rules.deleteConfirmTitle": "Regel verwijderen",
"rules.deleteFailed": "Verwijderen van regel is mislukt. Laad opnieuw.",
- "rules.disableFailed": "Kan regel niet uitschakelen. Laad opnieuw.",
"rules.empty": "Nog geen regel aangemaakt.",
"rules.emptyAddRule": "Regel toevoegen",
- "rules.enableFailed": "Kan regel niet inschakelen. Laad opnieuw.",
"rules.enqueued": "Regel is toegevoegd aan de wachtrij.",
"rules.itemPageTitle": "Rule",
"rules.listPageTitle": "Regels",
@@ -642,6 +643,7 @@
"rules.ruleEvents.nextAttemptLabel": "Volgende",
"rules.ruleEvents.numAttemptsLabel": "Pogingen",
"rules.ruleEvents.reloaded": "RuleEvents herladen.",
+ "rules.ruleSimulator.listPageTitle": "Simulator",
"rules.ruleSyntax.if": "If",
"rules.ruleSyntax.then": "then",
"rules.run": "Uitvoeren",
@@ -650,17 +652,16 @@
"rules.runningRule": "Regel '{name}' is momenteel actief.",
"rules.runRuleConfirmText": "Wil je de regel echt voor alle evenementen uitvoeren?",
"rules.runRuleConfirmTitle": "Regel uitvoeren",
+ "rules.simulate": "Simulate",
+ "rules.simulateTooltip": "Simulate this rules using the last 100 events.",
+ "rules.simulator": "Simulator",
"rules.stop": "Regel stopt binnenkort.",
"rules.triggerConfirmText": "Wil je echt de regel activeren?",
"rules.triggerConfirmTitle": "Trigger regel",
- "rules.triggerEdit": "Trigger bewerken",
"rules.triggerFailed": "Kan regel niet activeren. Laad opnieuw.",
+ "rules.triggerHint": "The selection of the trigger type cannot be changed later.",
"rules.unnamed": "Naamloos regel",
"rules.updateFailed": "Update regel mislukt. Laad opnieuw.",
- "rules.wizard.actionHint": "De selectie van het actietype kan later niet worden gewijzigd.",
- "rules.wizard.selectAction": "Selecteer actie",
- "rules.wizard.selectTrigger": "Selecteer Trigger",
- "rules.wizard.triggerHint": "De selectie van het triggertype kan later niet worden gewijzigd.",
"schemas.addField": "Veld toevoegen",
"schemas.addFieldAndClose": "Maken en sluiten",
"schemas.addFieldAndCreate": "Maak en voeg veld toe",
@@ -883,7 +884,7 @@
"tour.step1Next": "Doorgaan",
"tour.step1Text": "Een app is de opslagplaats voor uw project, bijvoorbeeld (blog, webshop of mobiele app). Je kunt bijdragers aan uw app toewijzen om samen te werken. \n \n Je kunt een onbeperkt aantal apps maken in Squidex om meerdere projecten tegelijkertijd te beheren. ",
"tour.step2Next": "Ga door!",
- "tour.step2Text": "Schema's bepalen de structuur van uw inhoud, de velden en de gegevenstypen van een inhoudsitem. \n \n Voordat je inhoud aan uw schema kunt toevoegen, moet je op de knop 'Publiceren' bovenaan klikken om het schema beschikbaar te maken voor uw inhoudeditors. ",
+ "tour.step2Text": "Schema's bepalen de structuur van uw inhoud, de velden en de gegevenstypen van een inhoudsitem. \n \n Voordat je inhoud aan uw schema kunt toevoegen, moet je op de knop 'Publiceren' bovenaan klikken om het schema beschikbaar te maken voor uw inhoudappSettings.editors. ",
"tour.step3Next": "Bijna klaar!",
"tour.step3Text": "Inhoud zijn de feitelijke gegevens in uw app die zijn gegroepeerd op basis van het schema. \n \n Selecteer eerst een gepubliceerd schema en voeg vervolgens inhoud toe voor dit schema.",
"tour.step4Next": "Begrepen!",
diff --git a/backend/i18n/source/frontend__ignore.json b/backend/i18n/source/frontend__ignore.json
index 4635dc8c0..f8729b10d 100644
--- a/backend/i18n/source/frontend__ignore.json
+++ b/backend/i18n/source/frontend__ignore.json
@@ -193,13 +193,13 @@
"/shared/services/schemas.types.ts": [
"Invalid properties type"
],
+ "/shared/state/appSettings.patterns.forms.ts": [
+ "[A-z0-9]+[A-z0-9\\- ]*[A-z0-9]"
+ ],
"/shared/state/contents.forms.visitors.ts": [
"",
"yyyy-MM-dd HH:mm:ss"
],
- "/shared/state/patterns.forms.ts": [
- "[A-z0-9]+[A-z0-9\\- ]*[A-z0-9]"
- ],
"/shared/state/query.ts": [
"ends with",
"is empty",
diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json
index 83d250a89..7daa8257a 100644
--- a/backend/i18n/source/frontend_en.json
+++ b/backend/i18n/source/frontend_en.json
@@ -41,9 +41,11 @@
"apps.leaveFailed": "Failed to leave app. Please reload.",
"apps.listPageTitle": "Apps",
"apps.loadFailed": "Failed to load apps. Please reload.",
+ "apps.loadSettingsFailed": "Failed to update UI settings. Please reload.",
"apps.removeImage": "Remove image",
"apps.removeImageFailed": "Failed to remove app image. Please reload.",
"apps.updateFailed": "Failed to update app. Please reload.",
+ "apps.updateSettingsFailed": "Faield to update UI settings. Please reload.",
"apps.upgradeHintCurrent": "You are on the {plan} plan.",
"apps.upgradeHintUpgrade": "Upgrade!",
"apps.uploadImage": "Drop an file to replace the app image. Use a square size.",
@@ -52,11 +54,18 @@
"apps.uploadImageTooBig": "App image is too big.",
"apps.welcomeSubtitle": "Welcome to Squidex.",
"apps.welcomeTitle": "Hi {user}",
+ "appSettings.editors.deleteConfirmText": "Do you really want to remove this Editor URL?",
+ "appSettings.editors.deleteConfirmTitle": "Delete Editor URL",
+ "appSettings.editors.empty": "No Editor URL created yet.",
+ "appSettings.editors.title": "Custom Editors",
"appSettings.hideScheduler": "Hide dialog for scheduled publishing",
+ "appSettings.patterns.deleteConfirmText": "Do you really want to remove this pattern?",
+ "appSettings.patterns.deleteConfirmTitle": "Delete pattern",
+ "appSettings.patterns.empty": "No pattern created yet.",
+ "appSettings.patterns.title": "Patterns",
"appSettings.refreshTooltip": "Refresh UI Settings (CTRL + SHIFT + R)",
"appSettings.reloaded": "UI Settings reloaded.",
"appSettings.title": "UI Settings",
- "appSettings.updateFailed": "Failed to update UI settings. Please reload.",
"assets.createFolder": "Create Folder",
"assets.createFolderFailed": "Failed to create asset folder. Please reload.",
"assets.createFolderTooltip": "Create new folder (CTRL + SHIFT + G)",
@@ -331,6 +340,7 @@
"common.settings": "Settings",
"common.sidebar": "Sidebar Extension",
"common.sidebarTour": "The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time.",
+ "common.skipped": "Skipped",
"common.slug": "Slug",
"common.stars.max": "Must not have more more than 15 stars",
"common.status": "Status",
@@ -528,10 +538,6 @@
"dashboard.trafficSummaryCard": "API Traffic Summary",
"dashboard.welcomeText": "Welcome to **{app}** dashboard.",
"dashboard.welcomeTitle": "Hi {user}",
- "editors.deleteConfirmText": "Do you really want to remove this Editor URL?",
- "editors.deleteConfirmTitle": "Delete Editor URL",
- "editors.empty": "No Editor URL created yet.",
- "editors.title": "Custom Editors",
"eventConsumers.count": "Count",
"eventConsumers.loadFailed": "Failed to load event consumers. Please reload.",
"eventConsumers.pageTitle": "Event Consumers",
@@ -563,10 +569,6 @@
"news.headline": "What's new?",
"news.title": "New Features",
"notifo.subscripeTooltip": "Click this button to subscribe to all changes and to receive push notifications.",
- "patterns.deleteConfirmText": "Do you really want to remove this pattern?",
- "patterns.deleteConfirmTitle": "Delete pattern",
- "patterns.empty": "No pattern created yet.",
- "patterns.title": "Patterns",
"plans.billingPortal": "Billing Portal",
"plans.billingPortalHint": "Go to Billing Portal for payment history and subscription overview.",
"plans.change": "Change",
@@ -590,7 +592,7 @@
"roles.addFailed": "Failed to add role. Please reload.",
"roles.default.owner": "Can do everything, including deleting the app.",
"roles.default.reader": "Can only read assets and contents.",
- "roles.defaults.developer": "Can use the API view, edit assets, contents, schemas, rules, workflows and patterns.",
+ "roles.defaults.developer": "Can use the API view, edit assets, contents, schemas, rules, workflows and appSettings.patterns.",
"roles.defaults.editor": "Can edit assets and contents and view workflows.",
"roles.deleteConfirmText": "Delete role",
"roles.deleteConfirmTitle": "Do you really want to delete the role?",
@@ -611,7 +613,8 @@
"roles.revokeFailed": "Failed to revoke role. Please reload.",
"roles.roleNamePlaceholder": "Enter role name",
"roles.updateFailed": "Failed to update role. Please reload.",
- "rules.actionEdit": "Edit Action",
+ "rules.actionData": "Action Data",
+ "rules.actionHint": "The selection of the action type cannot be changed later.",
"rules.cancelFailed": "Failed to cancel rule. Please reload.",
"rules.create": "New Rule",
"rules.createFailed": "Failed to create rule. Please reload.",
@@ -619,10 +622,8 @@
"rules.deleteConfirmText": "Do you really want to delete the rule?",
"rules.deleteConfirmTitle": "Delete rule",
"rules.deleteFailed": "Failed to delete rule. Please reload.",
- "rules.disableFailed": "Failed to disable rule. Please reload.",
"rules.empty": "No rule created yet.",
"rules.emptyAddRule": "Add Rule",
- "rules.enableFailed": "Failed to enable rule. Please reload.",
"rules.enqueued": "Rule has been added to the queue.",
"rules.itemPageTitle": "Rule",
"rules.listPageTitle": "Rules",
@@ -642,6 +643,7 @@
"rules.ruleEvents.nextAttemptLabel": "Next",
"rules.ruleEvents.numAttemptsLabel": "Attempts",
"rules.ruleEvents.reloaded": "RuleEvents reloaded.",
+ "rules.ruleSimulator.listPageTitle": "Simulator",
"rules.ruleSyntax.if": "If",
"rules.ruleSyntax.then": "then",
"rules.run": "Run",
@@ -650,17 +652,16 @@
"rules.runningRule": "Rule '{name}' is currently running.",
"rules.runRuleConfirmText": "Do you really want to run the rule for all events?",
"rules.runRuleConfirmTitle": "Run rule",
+ "rules.simulate": "Simulate",
+ "rules.simulateTooltip": "Simulate this rules using the last 100 events.",
+ "rules.simulator": "Simulator",
"rules.stop": "Rule will stop soon.",
"rules.triggerConfirmText": "Do you really want to trigger the rule?",
"rules.triggerConfirmTitle": "Trigger rule",
- "rules.triggerEdit": "Edit Trigger",
"rules.triggerFailed": "Failed to trigger rule. Please reload.",
+ "rules.triggerHint": "The selection of the trigger type cannot be changed later.",
"rules.unnamed": "Unnamed Rule",
"rules.updateFailed": "Failed to update rule. Please reload.",
- "rules.wizard.actionHint": "The selection of the action type cannot be changed later.",
- "rules.wizard.selectAction": "Select Action",
- "rules.wizard.selectTrigger": "Select Trigger",
- "rules.wizard.triggerHint": "The selection of the trigger type cannot be changed later.",
"schemas.addField": "Add Field",
"schemas.addFieldAndClose": "Create and close",
"schemas.addFieldAndCreate": "Create and add field",
@@ -883,7 +884,7 @@
"tour.step1Next": "Continue",
"tour.step1Text": "An App is the repository for your project, e.g. (blog, web shop or mobile app). You can assign contributors to your app to work together.\n\nYou can create an unlimited number of Apps in Squidex to manage multiple projects at the same time.",
"tour.step2Next": "Keep going!",
- "tour.step2Text": "Schemas define the structure of your content, the fields and the data types of a content item.\n\nBefore you can add content to your schema, make sure to hit the 'Publish' button at the top to make the schema available to your content editors.",
+ "tour.step2Text": "Schemas define the structure of your content, the fields and the data types of a content item.\n\nBefore you can add content to your schema, make sure to hit the 'Publish' button at the top to make the schema available to your content appSettings.editors.",
"tour.step3Next": "Almost there!",
"tour.step3Text": "Content is the actual data in your app which is grouped by the schema.\n\nSelect a published schema first, then add content for this schema.",
"tour.step4Next": "Got It!",
diff --git a/backend/i18n/source/frontend_it.json b/backend/i18n/source/frontend_it.json
index 788332b03..62aaa1801 100644
--- a/backend/i18n/source/frontend_it.json
+++ b/backend/i18n/source/frontend_it.json
@@ -52,6 +52,9 @@
"apps.uploadImageTooBig": "L'immagine dell'app è troppo grande.",
"apps.welcomeSubtitle": "Benvenuto su Squidex.",
"apps.welcomeTitle": "Ciao {user}",
+ "appSettings.patterns.deleteConfirmText": "Sei sicuro di voler rimuovere il pattern?",
+ "appSettings.patterns.deleteConfirmTitle": "Cancella il pattern",
+ "appSettings.patterns.empty": "Nessun pattern è stato ancora creato.",
"assets.createFolder": "Crea cartella",
"assets.createFolderFailed": "Non è stato possibile creare la cartella degli asset. Per favore ricarica.",
"assets.createFolderTooltip": "Crea una nuova cartella (CTRL + SHIFT + G)",
@@ -545,9 +548,6 @@
"news.headline": "Che cosa c'è di nuovo?",
"news.title": "Nuove funzionalità",
"notifo.subscripeTooltip": "Fai clic su questo pulsante per iscriverti a tutte le modifiche e ricevere le notifiche push.",
- "patterns.deleteConfirmText": "Sei sicuro di voler rimuovere il pattern?",
- "patterns.deleteConfirmTitle": "Cancella il pattern",
- "patterns.empty": "Nessun pattern è stato ancora creato.",
"plans.billingPortal": "Portale di fatturazione",
"plans.billingPortalHint": "Vai al portale di fatturazione per lo storico dei pagamenti e una panoramica per l'abbonamento.",
"plans.change": "Cambia",
diff --git a/backend/i18n/source/frontend_nl.json b/backend/i18n/source/frontend_nl.json
index 542821cb1..f6019dea5 100644
--- a/backend/i18n/source/frontend_nl.json
+++ b/backend/i18n/source/frontend_nl.json
@@ -48,6 +48,9 @@
"apps.uploadImageTooBig": "App-afbeelding is te groot.",
"apps.welcomeSubtitle": "Welkom bij Squidex.",
"apps.welcomeTitle": "Hallo {user}",
+ "appSettings.patterns.deleteConfirmText": "Wil je dit patroon echt verwijderen?",
+ "appSettings.patterns.deleteConfirmTitle": "Verwijder patroon",
+ "appSettings.patterns.empty": "Nog geen patroon gemaakt.",
"assets.createFolder": "Map maken",
"assets.createFolderFailed": "Maken van een map is mislukt. Laad opnieuw.",
"assets.createFolderTooltip": "Nieuwe map maken (CTRL + SHIFT + G)",
@@ -517,9 +520,6 @@
"news.headline": "Wat is er nieuw?",
"news.title": "Nieuwe functies",
"notifo.subscripeTooltip": "Klik op deze knop om je te abonneren op alle wijzigingen en om pushmeldingen te ontvangen.",
- "patterns.deleteConfirmText": "Wil je dit patroon echt verwijderen?",
- "patterns.deleteConfirmTitle": "Verwijder patroon",
- "patterns.empty": "Nog geen patroon gemaakt.",
"plans.billingPortal": "Factureringsportal",
"plans.billingPortalHint": "Ga naar het factureringsportaal voor betalingsgeschiedenis en abonnementsoverzicht.",
"plans.change": "Wijzigen",
@@ -820,7 +820,7 @@
"tour.step1Next": "Doorgaan",
"tour.step1Text": "Een app is de opslagplaats voor uw project, bijvoorbeeld (blog, webshop of mobiele app). Je kunt bijdragers aan uw app toewijzen om samen te werken. \n \n Je kunt een onbeperkt aantal apps maken in Squidex om meerdere projecten tegelijkertijd te beheren. ",
"tour.step2Next": "Ga door!",
- "tour.step2Text": "Schema's bepalen de structuur van uw inhoud, de velden en de gegevenstypen van een inhoudsitem. \n \n Voordat je inhoud aan uw schema kunt toevoegen, moet je op de knop 'Publiceren' bovenaan klikken om het schema beschikbaar te maken voor uw inhoudeditors. ",
+ "tour.step2Text": "Schema's bepalen de structuur van uw inhoud, de velden en de gegevenstypen van een inhoudsitem. \n \n Voordat je inhoud aan uw schema kunt toevoegen, moet je op de knop 'Publiceren' bovenaan klikken om het schema beschikbaar te maken voor uw inhoudappSettings.editors. ",
"tour.step3Next": "Bijna klaar!",
"tour.step3Text": "Inhoud zijn de feitelijke gegevens in uw app die zijn gegroepeerd op basis van het schema. \n \n Selecteer eerst een gepubliceerd schema en voeg vervolgens inhoud toe voor dit schema.",
"tour.step4Next": "Begrepen!",
diff --git a/backend/i18n/translate_en.bat b/backend/i18n/translate_en.bat
new file mode 100644
index 000000000..80987da02
--- /dev/null
+++ b/backend/i18n/translate_en.bat
@@ -0,0 +1,8 @@
+cd translator\Squidex.Translator
+
+dotnet run translate check-backend ..\..\..\.. -l en
+dotnet run translate check-frontend ..\..\..\.. -l en
+
+dotnet run translate gen-frontend ..\..\..\.. -l en
+dotnet run translate gen-backend ..\..\..\.. -l en
+
diff --git a/backend/i18n/translator/Squidex.Translator/Commands.cs b/backend/i18n/translator/Squidex.Translator/Commands.cs
index b57344f4e..64ea32a9d 100644
--- a/backend/i18n/translator/Squidex.Translator/Commands.cs
+++ b/backend/i18n/translator/Squidex.Translator/Commands.cs
@@ -6,13 +6,17 @@
// ==========================================================================
using System;
+using System.Collections.Generic;
using System.IO;
+using System.Linq;
using CommandDotNet;
using FluentValidation;
using FluentValidation.Attributes;
using Squidex.Translator.Processes;
using Squidex.Translator.State;
+#pragma warning disable CA1822 // Mark members as static
+
namespace Squidex.Translator
{
public class Commands
@@ -120,7 +124,19 @@ namespace Squidex.Translator
throw new ArgumentException("Folder does not exist.");
}
- var locales = new string[] { "en", "nl", "it" };
+ var supportedLocaled = new string[] { "en", "nl", "it" };
+
+ var locales = supportedLocaled;
+
+ if (arguments.Locales != null && arguments.Locales.Any())
+ {
+ locales = supportedLocaled.Intersect(arguments.Locales).ToArray();
+ }
+
+ if (locales.Length == 0)
+ {
+ locales = supportedLocaled;
+ }
var translationsDirectory = new DirectoryInfo(Path.Combine(arguments.Folder, "backend", "i18n"));
var translationsService = new TranslationService(translationsDirectory, fileName, locales, arguments.SingleWords);
@@ -141,6 +157,9 @@ namespace Squidex.Translator
[Option(LongName = "report", ShortName = "r")]
public bool Report { get; set; }
+ [Option(LongName = "locale", ShortName = "l")]
+ public IEnumerable Locales { get; set; }
+
public sealed class Validator : AbstractValidator
{
public Validator()
diff --git a/backend/i18n/translator/Squidex.Translator/Processes/Helper.cs b/backend/i18n/translator/Squidex.Translator/Processes/Helper.cs
index 998587374..b4e41f1f9 100644
--- a/backend/i18n/translator/Squidex.Translator/Processes/Helper.cs
+++ b/backend/i18n/translator/Squidex.Translator/Processes/Helper.cs
@@ -17,7 +17,7 @@ namespace Squidex.Translator.Processes
{
public static string RelativeName(FileInfo file, DirectoryInfo folder)
{
- return file.FullName.Substring(folder.FullName.Length).Replace("\\", "/");
+ return file.FullName[folder.FullName.Length..].Replace("\\", "/");
}
public static void CheckOtherLocales(TranslationService service)
diff --git a/backend/src/Migrations/Migrations/CreateAppSettings.cs b/backend/src/Migrations/Migrations/CreateAppSettings.cs
index acd5f1b7e..828cdae2d 100644
--- a/backend/src/Migrations/Migrations/CreateAppSettings.cs
+++ b/backend/src/Migrations/Migrations/CreateAppSettings.cs
@@ -41,7 +41,7 @@ namespace Migrations.Migrations
{
var apps = new Dictionary, Dictionary>();
- await eventStore.QueryAsync(storedEvent =>
+ await foreach (var storedEvent in eventStore.QueryAllAsync("^app\\-"))
{
var @event = eventDataFormatter.ParseIfKnown(storedEvent);
@@ -80,9 +80,7 @@ namespace Migrations.Migrations
}
}
}
-
- return Task.CompletedTask;
- }, "^app\\-");
+ }
var actor = RefToken.Client("Migrator");
diff --git a/backend/src/Migrations/Migrations/PopulateGrainIndexes.cs b/backend/src/Migrations/Migrations/PopulateGrainIndexes.cs
index c577a4d54..be2bb4981 100644
--- a/backend/src/Migrations/Migrations/PopulateGrainIndexes.cs
+++ b/backend/src/Migrations/Migrations/PopulateGrainIndexes.cs
@@ -75,7 +75,7 @@ namespace Migrations.Migrations
}
}
- await eventStore.QueryAsync(storedEvent =>
+ await foreach (var storedEvent in eventStore.QueryAllAsync("^app\\-"))
{
var @event = eventDataFormatter.ParseIfKnown(storedEvent);
@@ -109,9 +109,7 @@ namespace Migrations.Migrations
break;
}
}
-
- return Task.CompletedTask;
- }, "^app\\-");
+ }
await indexApps.RebuildAsync(appsByName);
@@ -130,7 +128,7 @@ namespace Migrations.Migrations
return rulesByApp!.GetOrAddNew(@event.AppId.Id);
}
- await eventStore.QueryAsync(storedEvent =>
+ await foreach (var storedEvent in eventStore.QueryAllAsync("^rule\\-"))
{
var @event = eventDataFormatter.ParseIfKnown(storedEvent);
@@ -146,9 +144,7 @@ namespace Migrations.Migrations
break;
}
}
-
- return Task.CompletedTask;
- }, "^rule\\-");
+ }
foreach (var (appId, rules) in rulesByApp)
{
@@ -165,7 +161,7 @@ namespace Migrations.Migrations
return schemasByApp!.GetOrAddNew(@event.AppId.Id);
}
- await eventStore.QueryAsync(storedEvent =>
+ await foreach (var storedEvent in eventStore.QueryAllAsync("^schema\\-"))
{
var @event = eventDataFormatter.ParseIfKnown(storedEvent);
@@ -181,9 +177,7 @@ namespace Migrations.Migrations
break;
}
}
-
- return Task.CompletedTask;
- }, "^schema\\-");
+ }
foreach (var (appId, schemas) in schemasByApp)
{
diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs
index aff64c83f..c9cd35ead 100644
--- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs
+++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs
@@ -13,7 +13,7 @@ namespace Squidex.Domain.Apps.Core.Rules.Triggers
[TypeName(nameof(ContentChangedTriggerV2))]
public sealed class ContentChangedTriggerV2 : RuleTrigger
{
- public ReadOnlyCollection Schemas { get; set; }
+ public ReadOnlyCollection? Schemas { get; set; }
public bool HandleAll { get; set; }
diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleService.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleService.cs
index c709d966e..115517830 100644
--- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleService.cs
+++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleService.cs
@@ -7,20 +7,22 @@
using System;
using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
-using Squidex.Domain.Apps.Core.Rules;
-using Squidex.Infrastructure;
+using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Core.HandleRules
{
public interface IRuleService
{
- bool CanCreateSnapshotEvents(Rule rule);
+ bool CanCreateSnapshotEvents(RuleContext context);
- IAsyncEnumerable<(RuleJob? Job, Exception? Exception)> CreateSnapshotJobsAsync(Rule rule, DomainId ruleId, DomainId appId);
+ string GetName(AppEvent @event);
- Task> CreateJobsAsync(Rule rule, DomainId ruleId, Envelope @event, bool ignoreStale = true);
+ IAsyncEnumerable CreateSnapshotJobsAsync(RuleContext context, CancellationToken ct = default);
+
+ IAsyncEnumerable CreateJobsAsync(Envelope @event, RuleContext context, CancellationToken ct = default);
Task<(Result Result, TimeSpan Elapsed)> InvokeAsync(string actionName, string job);
}
diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs
index 76e551cd1..e5fee620a 100644
--- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs
+++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs
@@ -7,11 +7,10 @@
using System;
using System.Collections.Generic;
-using System.Threading.Tasks;
-using Squidex.Domain.Apps.Core.Rules;
+using System.Linq;
+using System.Threading;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Events;
-using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Core.HandleRules
@@ -20,14 +19,33 @@ namespace Squidex.Domain.Apps.Core.HandleRules
{
Type TriggerType { get; }
- bool CanCreateSnapshotEvents { get; }
+ bool CanCreateSnapshotEvents
+ {
+ get => false;
+ }
- IAsyncEnumerable CreateSnapshotEvents(RuleTrigger trigger, DomainId appId);
+ IAsyncEnumerable CreateSnapshotEventsAsync(RuleContext context, CancellationToken ct)
+ {
+ return AsyncEnumerable.Empty();
+ }
- Task> CreateEnrichedEventsAsync(Envelope @event);
+ IAsyncEnumerable CreateEnrichedEventsAsync(Envelope @event, RuleContext context, CancellationToken ct);
- bool Trigger(EnrichedEvent @event, RuleTrigger trigger);
+ string? GetName(AppEvent @event)
+ {
+ return null;
+ }
- bool Trigger(AppEvent @event, RuleTrigger trigger, DomainId ruleId);
+ bool Trigger(Envelope @event, RuleContext context)
+ {
+ return true;
+ }
+
+ bool Trigger(EnrichedEvent @event, RuleContext context)
+ {
+ return true;
+ }
+
+ bool Handles(AppEvent @event);
}
}
diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/JobResult.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/JobResult.cs
new file mode 100644
index 000000000..9fde0e823
--- /dev/null
+++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/JobResult.cs
@@ -0,0 +1,26 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using Squidex.Domain.Apps.Core.Rules;
+
+#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
+
+namespace Squidex.Domain.Apps.Core.HandleRules
+{
+ public sealed record JobResult(RuleJob? Job, Exception? Exception = null, SkipReason SkipReason = default)
+ {
+ public static readonly JobResult ConditionDoesNotMatch = new JobResult(null, null, SkipReason.ConditionDoesNotMatch);
+ public static readonly JobResult Disabled = new JobResult(null, null, SkipReason.Disabled);
+ public static readonly JobResult EventMismatch = new JobResult(null, null, SkipReason.EventMismatch);
+ public static readonly JobResult FromRule = new JobResult(null, null, SkipReason.FromRule);
+ public static readonly JobResult NoAction = new JobResult(null, null, SkipReason.NoAction);
+ public static readonly JobResult NoTrigger = new JobResult(null, null, SkipReason.NoTrigger);
+ public static readonly JobResult TooOld = new JobResult(null, null, SkipReason.TooOld);
+ public static readonly JobResult WrongEventForTrigger = new JobResult(null, null, SkipReason.WrongEventForTrigger);
+ }
+}
diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleContext.cs
new file mode 100644
index 000000000..f74d27aa9
--- /dev/null
+++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleContext.cs
@@ -0,0 +1,23 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Squidex.Domain.Apps.Core.Rules;
+using Squidex.Infrastructure;
+
+namespace Squidex.Domain.Apps.Core.HandleRules
+{
+ public struct RuleContext
+ {
+ public NamedId AppId { get; init; }
+
+ public DomainId RuleId { get; init; }
+
+ public Rule Rule { get; init; }
+
+ public bool IgnoreStale { get; init; }
+ }
+}
diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
index 2724b3de5..6549b9d0e 100644
--- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
+++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
@@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
@@ -68,11 +69,11 @@ namespace Squidex.Domain.Apps.Core.HandleRules
this.log = log;
}
- public bool CanCreateSnapshotEvents(Rule rule)
+ public bool CanCreateSnapshotEvents(RuleContext context)
{
- Guard.NotNull(rule, nameof(rule));
+ Guard.NotNull(context.Rule, nameof(context.Rule));
- if (!ruleTriggerHandlers.TryGetValue(rule.Trigger.GetType(), out var triggerHandler))
+ if (!ruleTriggerHandlers.TryGetValue(context.Rule.Trigger.GetType(), out var triggerHandler))
{
return false;
}
@@ -80,8 +81,13 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return triggerHandler.CanCreateSnapshotEvents;
}
- public async IAsyncEnumerable<(RuleJob? Job, Exception? Exception)> CreateSnapshotJobsAsync(Rule rule, DomainId ruleId, DomainId appId)
+ public async IAsyncEnumerable CreateSnapshotJobsAsync(RuleContext context,
+ [EnumeratorCancellation] CancellationToken ct = default)
{
+ Guard.NotNull(context.Rule, nameof(context.Rule));
+
+ var rule = context.Rule;
+
if (!rule.IsEnabled)
{
yield break;
@@ -104,68 +110,93 @@ namespace Squidex.Domain.Apps.Core.HandleRules
var now = clock.GetCurrentInstant();
- await foreach (var enrichedEvent in triggerHandler.CreateSnapshotEvents(rule.Trigger, appId))
+ await foreach (var enrichedEvent in triggerHandler.CreateSnapshotEventsAsync(context, ct))
{
- Exception? exception;
-
- RuleJob? job = null;
-
+ JobResult? job;
try
{
await eventEnricher.EnrichAsync(enrichedEvent, null);
- if (!triggerHandler.Trigger(enrichedEvent, rule.Trigger))
+ if (!triggerHandler.Trigger(enrichedEvent, context))
{
continue;
}
- (job, exception) = await CreateJobAsync(rule, ruleId, actionHandler, now, enrichedEvent);
+ job = await CreateJobAsync(actionHandler, enrichedEvent, context, now);
}
catch (Exception ex)
{
- exception = ex;
+ job = new JobResult(null, ex);
}
- yield return (job, exception);
+ yield return job;
}
}
- public async Task> CreateJobsAsync(Rule rule, DomainId ruleId, Envelope @event, bool ignoreStale = true)
+ public async IAsyncEnumerable CreateJobsAsync(Envelope @event, RuleContext context,
+ [EnumeratorCancellation] CancellationToken ct = default)
{
- Guard.NotNull(rule, nameof(rule));
Guard.NotNull(@event, nameof(@event));
- var result = new List<(RuleJob Job, Exception? Exception)>();
+ var jobs = new List();
+
+ await AddJobsAsync(jobs, @event, context, ct);
+
+ foreach (var job in jobs)
+ {
+ if (ct.IsCancellationRequested)
+ {
+ break;
+ }
+
+ yield return job;
+ }
+ }
+ private async Task AddJobsAsync(List jobs, Envelope @event, RuleContext context, CancellationToken ct)
+ {
try
{
+ var rule = context.Rule;
+
if (!rule.IsEnabled)
{
- return result;
+ jobs.Add(JobResult.Disabled);
+ return;
}
if (@event.Payload is not AppEvent)
{
- return result;
+ jobs.Add(JobResult.EventMismatch);
+ return;
}
var typed = @event.To();
if (typed.Payload.FromRule)
{
- return result;
+ jobs.Add(JobResult.FromRule);
+ return;
}
var actionType = rule.Action.GetType();
if (!ruleTriggerHandlers.TryGetValue(rule.Trigger.GetType(), out var triggerHandler))
{
- return result;
+ jobs.Add(JobResult.NoTrigger);
+ return;
+ }
+
+ if (!triggerHandler.Handles(typed.Payload))
+ {
+ jobs.Add(JobResult.WrongEventForTrigger);
+ return;
}
if (!ruleActionHandlers.TryGetValue(actionType, out var actionHandler))
{
- return result;
+ jobs.Add(JobResult.NoAction);
+ return;
}
var now = clock.GetCurrentInstant();
@@ -175,37 +206,50 @@ namespace Squidex.Domain.Apps.Core.HandleRules
@event.Headers.Timestamp() :
now;
- if (ignoreStale && eventTime.Plus(Constants.StaleTime) < now)
+ if (context.IgnoreStale && eventTime.Plus(Constants.StaleTime) < now)
{
- return result;
+ jobs.Add(JobResult.TooOld);
+ return;
}
- if (!triggerHandler.Trigger(typed.Payload, rule.Trigger, ruleId))
+ if (!triggerHandler.Trigger(typed, context))
{
- return result;
+ jobs.Add(JobResult.ConditionDoesNotMatch);
+ return;
}
- var appEventEnvelope = @event.To();
-
- var enrichedEvents = await triggerHandler.CreateEnrichedEventsAsync(appEventEnvelope);
-
- foreach (var enrichedEvent in enrichedEvents)
+ await foreach (var enrichedEvent in triggerHandler.CreateEnrichedEventsAsync(typed, context, ct))
{
+ if (string.IsNullOrWhiteSpace(enrichedEvent.Name))
+ {
+ enrichedEvent.Name = GetName(typed.Payload);
+ }
+
try
{
await eventEnricher.EnrichAsync(enrichedEvent, typed);
- if (!triggerHandler.Trigger(enrichedEvent, rule.Trigger))
+ if (!triggerHandler.Trigger(enrichedEvent, context))
{
+ if (jobs.Count == 0)
+ {
+ jobs.Add(JobResult.ConditionDoesNotMatch);
+ }
+
continue;
}
- var (job, exception) = await CreateJobAsync(rule, ruleId, actionHandler, now, enrichedEvent);
+ var job = await CreateJobAsync(actionHandler, enrichedEvent, context, now);
- result.Add((job, exception));
+ jobs.Add(job);
}
catch (Exception ex)
{
+ if (jobs.Count == 0)
+ {
+ jobs.Add(new JobResult(null, ex, SkipReason.Failed));
+ }
+
log.LogError(ex, w => w
.WriteProperty("action", "createRuleJobFromEvent")
.WriteProperty("status", "Failed"));
@@ -214,17 +258,17 @@ namespace Squidex.Domain.Apps.Core.HandleRules
}
catch (Exception ex)
{
+ jobs.Add(new JobResult(null, ex, SkipReason.Failed));
+
log.LogError(ex, w => w
.WriteProperty("action", "createRuleJob")
.WriteProperty("status", "Failed"));
}
-
- return result;
}
- private async Task<(RuleJob, Exception?)> CreateJobAsync(Rule rule, DomainId ruleId, IRuleActionHandler actionHandler, Instant now, EnrichedEvent enrichedEvent)
+ private async Task CreateJobAsync(IRuleActionHandler actionHandler, EnrichedEvent enrichedEvent, RuleContext context, Instant now)
{
- var actionName = typeNameRegistry.GetName(rule.Action.GetType());
+ var actionName = typeNameRegistry.GetName(context.Rule.Action.GetType());
var expires = now.Plus(Constants.ExpirationTime);
@@ -238,12 +282,12 @@ namespace Squidex.Domain.Apps.Core.HandleRules
EventName = enrichedEvent.Name,
ExecutionPartition = enrichedEvent.Partition,
Expires = expires,
- RuleId = ruleId
+ RuleId = context.RuleId
};
try
{
- var (description, data) = await actionHandler.CreateJobAsync(enrichedEvent, rule.Action);
+ var (description, data) = await actionHandler.CreateJobAsync(enrichedEvent, context.Rule.Action);
var json = jsonSerializer.Serialize(data);
@@ -251,14 +295,32 @@ namespace Squidex.Domain.Apps.Core.HandleRules
job.ActionName = actionName;
job.Description = description;
- return (job, null);
+ return new JobResult(job, null);
}
catch (Exception ex)
{
job.Description = "Failed to create job";
- return (job, ex);
+ return new JobResult(job, ex);
+ }
+ }
+
+ public string GetName(AppEvent @event)
+ {
+ foreach (var handler in ruleTriggerHandlers.Values)
+ {
+ if (handler.Handles(@event))
+ {
+ var name = handler.GetName(@event);
+
+ if (!string.IsNullOrWhiteSpace(name))
+ {
+ return name;
+ }
+ }
}
+
+ return @event.GetType().Name;
}
public async Task<(Result Result, TimeSpan Elapsed)> InvokeAsync(string actionName, string job)
diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs
deleted file mode 100644
index d0bb8bfc0..000000000
--- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.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.Threading.Tasks;
-using Squidex.Domain.Apps.Core.Rules;
-using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
-using Squidex.Domain.Apps.Events;
-using Squidex.Infrastructure;
-using Squidex.Infrastructure.EventSourcing;
-
-namespace Squidex.Domain.Apps.Core.HandleRules
-{
- public abstract class RuleTriggerHandler : IRuleTriggerHandler
- where TTrigger : RuleTrigger
- where TEvent : AppEvent
- where TEnrichedEvent : EnrichedEvent
- {
- private readonly List emptyEnrichedEvents = new List();
-
- public Type TriggerType
- {
- get => typeof(TTrigger);
- }
-
- public virtual bool CanCreateSnapshotEvents
- {
- get => false;
- }
-
- public virtual async IAsyncEnumerable CreateSnapshotEvents(TTrigger trigger, DomainId appId)
- {
- await Task.Yield();
- yield break;
- }
-
- public virtual async Task> CreateEnrichedEventsAsync(Envelope @event)
- {
- var enrichedEvent = await CreateEnrichedEventAsync(@event.To());
-
- if (enrichedEvent != null)
- {
- return new List
- {
- enrichedEvent
- };
- }
- else
- {
- return emptyEnrichedEvents;
- }
- }
-
- IAsyncEnumerable IRuleTriggerHandler.CreateSnapshotEvents(RuleTrigger trigger, DomainId appId)
- {
- return CreateSnapshotEvents((TTrigger)trigger, appId);
- }
-
- 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, DomainId ruleId)
- {
- if (@event is TEvent typed)
- {
- return Trigger(typed, (TTrigger)trigger, ruleId);
- }
-
- return false;
- }
-
- protected virtual Task CreateEnrichedEventAsync(Envelope @event)
- {
- return Task.FromResult(null);
- }
-
- protected abstract bool Trigger(TEnrichedEvent @event, TTrigger trigger);
-
- protected virtual bool Trigger(TEvent @event, TTrigger trigger, DomainId ruleId)
- {
- return true;
- }
- }
-}
diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/SkipReason.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/SkipReason.cs
new file mode 100644
index 000000000..43940bef1
--- /dev/null
+++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/SkipReason.cs
@@ -0,0 +1,23 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+namespace Squidex.Domain.Apps.Core.HandleRules
+{
+ public enum SkipReason
+ {
+ None,
+ ConditionDoesNotMatch,
+ Disabled,
+ EventMismatch,
+ Failed,
+ FromRule,
+ NoAction,
+ NoTrigger,
+ TooOld,
+ WrongEventForTrigger
+ }
+}
\ No newline at end of file
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
index 25c79dcb3..0eb68127b 100644
--- 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
@@ -27,6 +27,7 @@
+
diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
index 8e0e7a8b8..f1086ad30 100644
--- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
@@ -7,6 +7,7 @@
using System.Collections.Generic;
using System.Linq;
+using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
@@ -68,13 +69,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
}, ct);
}
- public async IAsyncEnumerable StreamAll(DomainId appId)
+ public async IAsyncEnumerable StreamAll(DomainId appId,
+ [EnumeratorCancellation] CancellationToken ct = default)
{
var find = Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted);
- using (var cursor = await find.ToCursorAsync())
+ using (var cursor = await find.ToCursorAsync(ct))
{
- while (await cursor.MoveNextAsync())
+ while (await cursor.MoveNextAsync(ct))
{
foreach (var entity in cursor.Current)
{
diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
index e8a405b31..dbbbcf721 100644
--- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
@@ -82,9 +82,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
await queryScheduled.PrepareAsync(collection, skipIndex, ct);
}
- public IAsyncEnumerable StreamAll(DomainId appId, HashSet? schemaIds)
+ public IAsyncEnumerable StreamAll(DomainId appId, HashSet? schemaIds, CancellationToken ct)
{
- return queryAsStream.StreamAll(appId, schemaIds);
+ return queryAsStream.StreamAll(appId, schemaIds, ct);
}
public async Task> QueryAsync(IAppEntity app, List schemas, Q q)
diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
index 648c28f3b..c863a1568 100644
--- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
@@ -55,9 +55,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
await collectionPublished.InitializeAsync(ct);
}
- public IAsyncEnumerable StreamAll(DomainId appId, HashSet? schemaIds)
+ public IAsyncEnumerable StreamAll(DomainId appId, HashSet? schemaIds, CancellationToken ct)
{
- return collectionAll.StreamAll(appId, schemaIds);
+ return collectionAll.StreamAll(appId, schemaIds, ct);
}
public Task> QueryAsync(IAppEntity app, List schemas, Q q, SearchScope scope)
diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryAsStream.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryAsStream.cs
index f87844456..3bfb779e4 100644
--- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryAsStream.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryAsStream.cs
@@ -6,6 +6,7 @@
// ==========================================================================
using System.Collections.Generic;
+using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
@@ -27,16 +28,17 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
await Collection.Indexes.CreateOneAsync(indexBySchema, cancellationToken: ct);
}
- public async IAsyncEnumerable StreamAll(DomainId appId, HashSet? schemaIds)
+ public async IAsyncEnumerable StreamAll(DomainId appId, HashSet? schemaIds,
+ [EnumeratorCancellation] CancellationToken ct)
{
var find =
schemaIds != null ?
Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && schemaIds.Contains(x.IndexedSchemaId)) :
Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted);
- using (var cursor = await find.ToCursorAsync())
+ using (var cursor = await find.ToCursorAsync(ct))
{
- while (await cursor.MoveNextAsync())
+ while (await cursor.MoveNextAsync(ct))
{
foreach (var entity in cursor.Current)
{
diff --git a/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs
index 20c97fb2a..05afab815 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs
@@ -169,6 +169,13 @@ namespace Squidex.Domain.Apps.Entities
return rules.ToList();
}
+ public async Task GetRuleAsync(DomainId appId, DomainId id)
+ {
+ var rules = await GetRulesAsync(appId);
+
+ return rules.Find(x => x.Id == id);
+ }
+
private static string AppCacheKey(DomainId appId)
{
return $"APPS_ID_{appId}";
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs
index 06329e5f9..a247b8dfe 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs
@@ -5,13 +5,16 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
+using System;
using System.Collections.Generic;
-using System.Threading.Tasks;
+using System.Runtime.CompilerServices;
+using System.Threading;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
+using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
@@ -19,13 +22,15 @@ using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Assets
{
- public sealed class AssetChangedTriggerHandler : RuleTriggerHandler
+ public sealed class AssetChangedTriggerHandler : IRuleTriggerHandler
{
private readonly IScriptEngine scriptEngine;
private readonly IAssetLoader assetLoader;
private readonly IAssetRepository assetRepository;
- public override bool CanCreateSnapshotEvents => true;
+ public bool CanCreateSnapshotEvents => true;
+
+ public Type TriggerType => typeof(AssetChangedTriggerV2);
public AssetChangedTriggerHandler(
IScriptEngine scriptEngine,
@@ -41,9 +46,15 @@ namespace Squidex.Domain.Apps.Entities.Assets
this.assetRepository = assetRepository;
}
- public override async IAsyncEnumerable CreateSnapshotEvents(AssetChangedTriggerV2 trigger, DomainId appId)
+ public bool Handles(AppEvent @event)
+ {
+ return @event is AssetEvent && @event is not AssetMoved;
+ }
+
+ public async IAsyncEnumerable CreateSnapshotEventsAsync(RuleContext context,
+ [EnumeratorCancellation] CancellationToken ct = default)
{
- await foreach (var asset in assetRepository.StreamAll(appId))
+ await foreach (var asset in assetRepository.StreamAll(context.AppId.Id, ct))
{
var result = new EnrichedAssetEvent
{
@@ -53,24 +64,22 @@ namespace Squidex.Domain.Apps.Entities.Assets
SimpleMapper.Map(asset, result);
result.Actor = asset.LastModifiedBy;
- result.Name = "AssetCreatedFromSnapshot";
+ result.Name = "AssetQueried";
yield return result;
}
}
- protected override async Task CreateEnrichedEventAsync(Envelope @event)
+ public async IAsyncEnumerable CreateEnrichedEventsAsync(Envelope @event, RuleContext context,
+ [EnumeratorCancellation] CancellationToken ct = default)
{
- if (@event.Payload is AssetMoved)
- {
- return null;
- }
+ var assetEvent = (AssetEvent)@event.Payload;
var result = new EnrichedAssetEvent();
var asset = await assetLoader.GetAsync(
- @event.Payload.AppId.Id,
- @event.Payload.AssetId,
+ assetEvent.AppId.Id,
+ assetEvent.AssetId,
@event.Headers.EventStreamNumber());
if (asset != null)
@@ -96,13 +105,13 @@ namespace Squidex.Domain.Apps.Entities.Assets
break;
}
- result.Name = $"Asset{result.Type}";
-
- return result;
+ yield return result;
}
- protected override bool Trigger(EnrichedAssetEvent @event, AssetChangedTriggerV2 trigger)
+ public bool Trigger(EnrichedEvent @event, RuleContext context)
{
+ var trigger = (AssetChangedTriggerV2)context.Rule.Trigger;
+
if (string.IsNullOrWhiteSpace(trigger.Condition))
{
return true;
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/RepairFiles.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/RepairFiles.cs
index 1a6fe35be..55831f930 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/Assets/RepairFiles.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/RepairFiles.cs
@@ -39,7 +39,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
public async Task RepairAsync(CancellationToken ct = default)
{
- await eventStore.QueryAsync(async storedEvent =>
+ await foreach (var storedEvent in eventStore.QueryAllAsync("^asset\\-", ct: ct))
{
var @event = eventDataFormatter.ParseIfKnown(storedEvent);
@@ -55,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
break;
}
}
- }, "^asset\\-", ct: ct);
+ }
}
private async Task TryRepairAsync(NamedId appId, DomainId id, long fileVersion, CancellationToken ct)
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
index d085d026d..edcd3d1b2 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
@@ -6,6 +6,7 @@
// ==========================================================================
using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
using Squidex.Infrastructure;
@@ -13,7 +14,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories
{
public interface IAssetRepository
{
- IAsyncEnumerable StreamAll(DomainId appId);
+ IAsyncEnumerable StreamAll(DomainId appId, CancellationToken ct);
Task> QueryAsync(DomainId appId, DomainId? parentId, Q q);
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs
index f075898f0..23472eed0 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs
@@ -144,7 +144,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
var context = new BackupContext(appId, userMapping, writer);
- await eventStore.QueryAsync(async storedEvent =>
+ await foreach (var storedEvent in eventStore.QueryAllAsync(GetFilter(), ct: ct))
{
var @event = eventDataFormatter.Parse(storedEvent);
@@ -164,7 +164,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
job.HandledAssets = writer.WrittenAttachments;
lastTimestamp = await WritePeriodically(lastTimestamp);
- }, GetFilter(), null, ct);
+ }
foreach (var handler in handlers)
{
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs
index 185a623a5..6e7269c4a 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs
@@ -5,8 +5,10 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
+using System;
using System.Collections.Generic;
-using System.Threading.Tasks;
+using System.Runtime.CompilerServices;
+using System.Threading;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers;
@@ -20,12 +22,13 @@ using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Entities.Comments
{
- public sealed class CommentTriggerHandler : RuleTriggerHandler
+ public sealed class CommentTriggerHandler : IRuleTriggerHandler
{
- private static readonly List EmptyResult = new List();
private readonly IScriptEngine scriptEngine;
private readonly IUserResolver userResolver;
+ public Type TriggerType => typeof(CommentTrigger);
+
public CommentTriggerHandler(IScriptEngine scriptEngine, IUserResolver userResolver)
{
Guard.NotNull(scriptEngine, nameof(scriptEngine));
@@ -36,18 +39,22 @@ namespace Squidex.Domain.Apps.Entities.Comments
this.userResolver = userResolver;
}
- public override async Task> CreateEnrichedEventsAsync(Envelope @event)
+ public bool Handles(AppEvent @event)
+ {
+ return @event is CommentCreated;
+ }
+
+ public async IAsyncEnumerable CreateEnrichedEventsAsync(Envelope @event, RuleContext context,
+ [EnumeratorCancellation] CancellationToken ct)
{
- var commentCreated = @event.Payload as CommentCreated;
+ var commentCreated = (CommentCreated)@event.Payload;
- if (commentCreated?.Mentions?.Length > 0)
+ if (commentCreated.Mentions?.Length > 0)
{
var users = await userResolver.QueryManyAsync(commentCreated.Mentions);
if (users.Count > 0)
{
- var result = new List();
-
foreach (var user in users.Values)
{
var enrichedEvent = new EnrichedCommentEvent
@@ -59,18 +66,16 @@ namespace Squidex.Domain.Apps.Entities.Comments
SimpleMapper.Map(commentCreated, enrichedEvent);
- result.Add(enrichedEvent);
+ yield return enrichedEvent;
}
-
- return result;
}
}
-
- return EmptyResult;
}
- protected override bool Trigger(EnrichedCommentEvent @event, CommentTrigger trigger)
+ public bool Trigger(EnrichedEvent @event, RuleContext context)
{
+ var trigger = (CommentTrigger)context.Rule.Trigger;
+
if (string.IsNullOrWhiteSpace(trigger.Condition))
{
return true;
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs
index 4bf0d54ee..e33a8ec75 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs
@@ -5,15 +5,18 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
+using System;
using System.Collections.Generic;
using System.Linq;
-using System.Threading.Tasks;
+using System.Runtime.CompilerServices;
+using System.Threading;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
+using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
@@ -22,13 +25,20 @@ using Squidex.Text;
namespace Squidex.Domain.Apps.Entities.Contents
{
- public sealed class ContentChangedTriggerHandler : RuleTriggerHandler
+ public sealed class ContentChangedTriggerHandler : IRuleTriggerHandler
{
private readonly IScriptEngine scriptEngine;
private readonly IContentLoader contentLoader;
private readonly IContentRepository contentRepository;
- public override bool CanCreateSnapshotEvents => true;
+ public bool CanCreateSnapshotEvents => true;
+
+ public Type TriggerType => typeof(ContentChangedTriggerV2);
+
+ public bool Handles(AppEvent appEvent)
+ {
+ return appEvent is ContentEvent;
+ }
public ContentChangedTriggerHandler(
IScriptEngine scriptEngine,
@@ -44,14 +54,17 @@ namespace Squidex.Domain.Apps.Entities.Contents
this.contentRepository = contentRepository;
}
- public override async IAsyncEnumerable CreateSnapshotEvents(ContentChangedTriggerV2 trigger, DomainId appId)
+ public async IAsyncEnumerable CreateSnapshotEventsAsync(RuleContext context,
+ [EnumeratorCancellation] CancellationToken ct = default)
{
+ var trigger = (ContentChangedTriggerV2)context.Rule.Trigger;
+
var schemaIds =
trigger.Schemas?.Count > 0 ?
trigger.Schemas.Select(x => x.SchemaId).Distinct().ToHashSet() :
null;
- await foreach (var content in contentRepository.StreamAll(appId, schemaIds))
+ await foreach (var content in contentRepository.StreamAll(context.AppId.Id, schemaIds, ct))
{
var result = new EnrichedContentEvent
{
@@ -61,20 +74,23 @@ namespace Squidex.Domain.Apps.Entities.Contents
SimpleMapper.Map(content, result);
result.Actor = content.LastModifiedBy;
- result.Name = $"{content.SchemaId.Name.ToPascalCase()}CreatedFromSnapshot";
+ result.Name = $"ContentQueried({content.SchemaId.Name.ToPascalCase()})";
yield return result;
}
}
- protected override async Task CreateEnrichedEventAsync(Envelope @event)
+ public async IAsyncEnumerable CreateEnrichedEventsAsync(Envelope @event, RuleContext context,
+ [EnumeratorCancellation] CancellationToken ct = default)
{
+ var contentEvent = (ContentEvent)@event.Payload;
+
var result = new EnrichedContentEvent();
var content =
await contentLoader.GetAsync(
- @event.Payload.AppId.Id,
- @event.Payload.ContentId,
+ contentEvent.AppId.Id,
+ contentEvent.ContentId,
@event.Headers.EventStreamNumber());
if (content != null)
@@ -131,13 +147,20 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
}
- result.Name = $"{@event.Payload.SchemaId.Name.ToPascalCase()}{result.Type}";
+ yield return result;
+ }
- return result;
+ public string? GetName(AppEvent @event)
+ {
+ var contentEvent = (ContentEvent)@event;
+
+ return $"{@event.GetType().Name}({contentEvent.SchemaId.Name.ToPascalCase()})";
}
- protected override bool Trigger(ContentEvent @event, ContentChangedTriggerV2 trigger, DomainId ruleId)
+ public bool Trigger(Envelope @event, RuleContext context)
{
+ var trigger = (ContentChangedTriggerV2)context.Rule.Trigger;
+
if (trigger.HandleAll)
{
return true;
@@ -145,9 +168,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (trigger.Schemas != null)
{
+ var contentEvent = (ContentEvent)@event.Payload;
+
foreach (var schema in trigger.Schemas)
{
- if (MatchsSchema(schema, @event.SchemaId))
+ if (MatchsSchema(schema, contentEvent.SchemaId))
{
return true;
}
@@ -157,8 +182,10 @@ namespace Squidex.Domain.Apps.Entities.Contents
return false;
}
- protected override bool Trigger(EnrichedContentEvent @event, ContentChangedTriggerV2 trigger)
+ public bool Trigger(EnrichedEvent @event, RuleContext context)
{
+ var trigger = (ContentChangedTriggerV2)context.Rule.Trigger;
+
if (trigger.HandleAll)
{
return true;
@@ -166,9 +193,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (trigger.Schemas != null)
{
+ var contentEvent = (EnrichedContentEvent)@event;
+
foreach (var schema in trigger.Schemas)
{
- if (MatchsSchema(schema, @event.SchemaId) && MatchsCondition(schema, @event))
+ if (MatchsSchema(schema, contentEvent.SchemaId) && MatchsCondition(schema, contentEvent))
{
return true;
}
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
index 4622bf10a..66fe44a66 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
@@ -7,6 +7,7 @@
using System;
using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
@@ -19,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories
{
public interface IContentRepository
{
- IAsyncEnumerable StreamAll(DomainId appId, HashSet? schemaIds);
+ IAsyncEnumerable StreamAll(DomainId appId, HashSet? schemaIds, CancellationToken ct);
Task> QueryAsync(IAppEntity app, List schemas, Q q, SearchScope scope);
diff --git a/backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs
index 11762f035..5a30918c7 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs
@@ -32,5 +32,7 @@ namespace Squidex.Domain.Apps.Entities
Task> GetSchemasAsync(DomainId appId);
Task> GetRulesAsync(DomainId appId);
+
+ Task GetRuleAsync(DomainId appId, DomainId id);
}
}
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs
index 6d12c5445..a081c499e 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs
@@ -114,7 +114,10 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject
private async Task Trigger(TriggerRule command)
{
- var @event = SimpleMapper.Map(command, new RuleManuallyTriggered { RuleId = Snapshot.Id, AppId = Snapshot.AppId });
+ var @event = new RuleManuallyTriggered();
+
+ SimpleMapper.Map(command, @event);
+ SimpleMapper.Map(Snapshot, @event);
await ruleEnqueuer.EnqueueAsync(Snapshot.RuleDef, Snapshot.Id, Envelope.Create(@event));
}
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs
index 3a6245d2b..56662aaaf 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs
@@ -5,33 +5,45 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Threading;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers;
+using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Rules;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Rules
{
- public sealed class ManualTriggerHandler : RuleTriggerHandler
+ public sealed class ManualTriggerHandler : IRuleTriggerHandler
{
- protected override Task CreateEnrichedEventAsync(Envelope @event)
+ public Type TriggerType => typeof(ManualTrigger);
+
+ public bool Handles(AppEvent appEvent)
{
- var result = new EnrichedManualEvent
- {
- Name = "Manual"
- };
+ return appEvent is RuleManuallyTriggered;
+ }
+
+ public async IAsyncEnumerable CreateEnrichedEventsAsync(Envelope @event, RuleContext context,
+ [EnumeratorCancellation] CancellationToken ct)
+ {
+ var result = new EnrichedManualEvent();
SimpleMapper.Map(@event.Payload, result);
- return Task.FromResult(result);
+ await Task.Yield();
+
+ yield return result;
}
- protected override bool Trigger(EnrichedManualEvent @event, ManualTrigger trigger)
+ public string? GetName(AppEvent @event)
{
- return true;
+ return "Manual";
}
}
}
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs
index 80c74fbe8..76d8f6610 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs
@@ -56,11 +56,21 @@ namespace Squidex.Domain.Apps.Entities.Rules
Guard.NotNull(rule, nameof(rule));
Guard.NotNull(@event, nameof(@event));
- var jobs = await ruleService.CreateJobsAsync(rule, ruleId, @event);
+ var ruleContext = new RuleContext
+ {
+ Rule = rule,
+ RuleId = ruleId,
+ IgnoreStale = false
+ };
+
+ var jobs = ruleService.CreateJobsAsync(@event, ruleContext);
- foreach (var (job, ex) in jobs)
+ await foreach (var (job, ex, _) in jobs)
{
- await ruleEventRepository.EnqueueAsync(job, ex);
+ if (job != null)
+ {
+ await ruleEventRepository.EnqueueAsync(job, ex);
+ }
}
}
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs
new file mode 100644
index 000000000..666cef073
--- /dev/null
+++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs
@@ -0,0 +1,128 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Orleans;
+using Squidex.Domain.Apps.Core.HandleRules;
+using Squidex.Domain.Apps.Core.Rules.Triggers;
+using Squidex.Domain.Apps.Events;
+using Squidex.Infrastructure;
+using Squidex.Infrastructure.EventSourcing;
+
+namespace Squidex.Domain.Apps.Entities.Rules.Runner
+{
+ public sealed class DefaultRuleRunnerService : IRuleRunnerService
+ {
+ private const int MaxSimulatedEvents = 100;
+ private readonly IEventDataFormatter eventDataFormatter;
+ private readonly IEventStore eventStore;
+ private readonly IGrainFactory grainFactory;
+ private readonly IRuleService ruleService;
+
+ public DefaultRuleRunnerService(IGrainFactory grainFactory,
+ IEventStore eventStore,
+ IEventDataFormatter eventDataFormatter,
+ IRuleService ruleService)
+ {
+ Guard.NotNull(grainFactory, nameof(grainFactory));
+ Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter));
+ Guard.NotNull(eventStore, nameof(eventStore));
+ Guard.NotNull(ruleService, nameof(ruleService));
+
+ this.grainFactory = grainFactory;
+ this.eventDataFormatter = eventDataFormatter;
+ this.eventStore = eventStore;
+ this.ruleService = ruleService;
+ }
+
+ public async Task> SimulateAsync(IRuleEntity rule, CancellationToken ct)
+ {
+ Guard.NotNull(rule, nameof(rule));
+
+ var context = GetContext(rule);
+
+ var result = new List(MaxSimulatedEvents);
+
+ await foreach (var storedEvent in eventStore.QueryAllReverseAsync($"^([a-z]+)\\-{rule.AppId.Id}", null, MaxSimulatedEvents, ct))
+ {
+ var @event = eventDataFormatter.ParseIfKnown(storedEvent);
+
+ if (@event?.Payload is AppEvent appEvent)
+ {
+ await foreach (var (job, exception, skip) in ruleService.CreateJobsAsync(@event, context, ct))
+ {
+ var name = job?.EventName;
+
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ name = ruleService.GetName(appEvent);
+ }
+
+ var simulationResult = new SimulatedRuleEvent(
+ name,
+ job?.ActionName,
+ job?.ActionData,
+ exception?.Message,
+ skip);
+
+ result.Add(simulationResult);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ public Task CancelAsync(DomainId appId)
+ {
+ var grain = grainFactory.GetGrain(appId.ToString());
+
+ return grain.CancelAsync();
+ }
+
+ public bool CanRunRule(IRuleEntity rule)
+ {
+ var context = GetContext(rule);
+
+ return context.Rule.IsEnabled && context.Rule.Trigger is not ManualTrigger;
+ }
+
+ public bool CanRunFromSnapshots(IRuleEntity rule)
+ {
+ var context = GetContext(rule);
+
+ return CanRunRule(rule) && ruleService.CanCreateSnapshotEvents(context);
+ }
+
+ public Task GetRunningRuleIdAsync(DomainId appId)
+ {
+ var grain = grainFactory.GetGrain(appId.ToString());
+
+ return grain.GetRunningRuleIdAsync();
+ }
+
+ public Task RunAsync(DomainId appId, DomainId ruleId, bool fromSnapshots = false)
+ {
+ var grain = grainFactory.GetGrain(appId.ToString());
+
+ return grain.RunAsync(ruleId, fromSnapshots);
+ }
+
+ private static RuleContext GetContext(IRuleEntity rule)
+ {
+ return new RuleContext
+ {
+ AppId = rule.AppId,
+ Rule = rule.RuleDef,
+ RuleId = rule.Id,
+ IgnoreStale = true
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/GrainRuleRunnerService.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/GrainRuleRunnerService.cs
deleted file mode 100644
index 4c1f59579..000000000
--- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/GrainRuleRunnerService.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 Orleans;
-using Squidex.Domain.Apps.Core.HandleRules;
-using Squidex.Domain.Apps.Core.Rules.Triggers;
-using Squidex.Infrastructure;
-
-namespace Squidex.Domain.Apps.Entities.Rules.Runner
-{
- public sealed class GrainRuleRunnerService : IRuleRunnerService
- {
- private readonly IGrainFactory grainFactory;
- private readonly IRuleService ruleService;
-
- public GrainRuleRunnerService(IGrainFactory grainFactory, IRuleService ruleService)
- {
- Guard.NotNull(grainFactory, nameof(grainFactory));
- Guard.NotNull(ruleService, nameof(ruleService));
-
- this.grainFactory = grainFactory;
- this.ruleService = ruleService;
- }
-
- public Task CancelAsync(DomainId appId)
- {
- var grain = grainFactory.GetGrain(appId.ToString());
-
- return grain.CancelAsync();
- }
-
- public bool CanRunRule(IRuleEntity rule)
- {
- return rule.RuleDef.IsEnabled && rule.RuleDef.Trigger is not ManualTrigger;
- }
-
- public bool CanRunFromSnapshots(IRuleEntity rule)
- {
- return CanRunRule(rule) && ruleService.CanCreateSnapshotEvents(rule.RuleDef);
- }
-
- public Task GetRunningRuleIdAsync(DomainId appId)
- {
- var grain = grainFactory.GetGrain(appId.ToString());
-
- return grain.GetRunningRuleIdAsync();
- }
-
- public Task RunAsync(DomainId appId, DomainId ruleId, bool fromSnapshots = false)
- {
- var grain = grainFactory.GetGrain(appId.ToString());
-
- return grain.RunAsync(ruleId, fromSnapshots);
- }
- }
-}
\ No newline at end of file
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs
index d4f5651ae..5cb073914 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs
@@ -5,6 +5,8 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
+using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
using Squidex.Infrastructure;
@@ -12,6 +14,8 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner
{
public interface IRuleRunnerService
{
+ Task> SimulateAsync(IRuleEntity rule, CancellationToken ct);
+
Task RunAsync(DomainId appId, DomainId ruleId, bool fromSnapshots = false);
Task CancelAsync(DomainId appId);
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs
index 7261e90b4..507c854bc 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs
@@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner
public string? Position { get; set; }
- public bool FromSnapshots { get; set; }
+ public bool RunFromSnapshots { get; set; }
}
public RuleRunnerGrain(
@@ -122,7 +122,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner
state.Value = new State
{
RuleId = ruleId,
- FromSnapshots = fromSnapshots
+ RunFromSnapshots = fromSnapshots
};
await EnsureIsRunningAsync(false);
@@ -136,7 +136,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner
if (job.RuleId != null && currentJobToken == null)
{
- if (state.Value.FromSnapshots && continues)
+ if (state.Value.RunFromSnapshots && continues)
{
state.Value = new State();
@@ -162,24 +162,30 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner
{
currentReminder = await RegisterOrUpdateReminder("KeepAlive", TimeSpan.Zero, TimeSpan.FromMinutes(2));
- var rules = await appProvider.GetRulesAsync(DomainId.Create(Key));
-
- var rule = rules.Find(x => x.Id == currentState.RuleId);
+ var rule = await appProvider.GetRuleAsync(DomainId.Create(Key), currentState.RuleId!.Value);
if (rule == null)
{
- throw new InvalidOperationException("Cannot find rule.");
+ throw new DomainObjectNotFoundException(currentState.RuleId.ToString()!);
}
using (localCache.StartContext())
{
- if (currentState.FromSnapshots && ruleService.CanCreateSnapshotEvents(rule.RuleDef))
+ var context = new RuleContext
+ {
+ AppId = rule.AppId,
+ Rule = rule.RuleDef,
+ RuleId = rule.Id,
+ IgnoreStale = true
+ };
+
+ if (currentState.RunFromSnapshots && ruleService.CanCreateSnapshotEvents(context))
{
- await EnqueueFromSnapshotsAsync(rule);
+ await EnqueueFromSnapshotsAsync(context, ct);
}
else
{
- await EnqueueFromEventsAsync(currentState, rule, ct);
+ await EnqueueFromEventsAsync(currentState, context, ct);
}
}
}
@@ -216,11 +222,11 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner
}
}
- private async Task EnqueueFromSnapshotsAsync(IRuleEntity rule)
+ private async Task EnqueueFromSnapshotsAsync(RuleContext context, CancellationToken ct)
{
var errors = 0;
- await foreach (var (job, ex) in ruleService.CreateSnapshotJobsAsync(rule.RuleDef, rule.Id, rule.AppId.Id))
+ await foreach (var (job, ex, _) in ruleService.CreateSnapshotJobsAsync(context, ct))
{
if (job != null)
{
@@ -242,11 +248,13 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner
}
}
- private async Task EnqueueFromEventsAsync(State currentState, IRuleEntity rule, CancellationToken ct)
+ private async Task EnqueueFromEventsAsync(State currentState, RuleContext context, CancellationToken ct)
{
var errors = 0;
- await eventStore.QueryAsync(async storedEvent =>
+ var filter = $"^([a-z]+)\\-{Key}";
+
+ await foreach (var storedEvent in eventStore.QueryAllAsync(filter, currentState.Position, ct: ct))
{
try
{
@@ -254,11 +262,14 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner
if (@event != null)
{
- var jobs = await ruleService.CreateJobsAsync(rule.RuleDef, rule.Id, @event, false);
+ var jobs = ruleService.CreateJobsAsync(@event, context, ct);
- foreach (var (job, ex) in jobs)
+ await foreach (var (job, ex, _) in jobs)
{
- await ruleEventRepository.EnqueueAsync(job, ex);
+ if (job != null)
+ {
+ await ruleEventRepository.EnqueueAsync(job, ex);
+ }
}
}
}
@@ -281,7 +292,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner
}
await state.WriteAsync();
- }, $"^([a-z]+)\\-{Key}", currentState.Position, ct);
+ }
}
public Task ReceiveReminder(string reminderName, TickStatus status)
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/SimulatedRuleEvent.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/SimulatedRuleEvent.cs
new file mode 100644
index 000000000..03ba31373
--- /dev/null
+++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/SimulatedRuleEvent.cs
@@ -0,0 +1,22 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Squidex.Domain.Apps.Core.HandleRules;
+
+#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
+
+namespace Squidex.Domain.Apps.Entities.Rules.Runner
+{
+ public sealed record SimulatedRuleEvent(
+ string EventName,
+ string? ActionName,
+ string? ActionData,
+ string? Error,
+ SkipReason SkipReason)
+ {
+ }
+}
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs
index 58650bae2..c5053f1fd 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs
@@ -5,6 +5,10 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Threading;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
@@ -14,25 +18,41 @@ using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
{
- public sealed class UsageTriggerHandler : RuleTriggerHandler
+ public sealed class UsageTriggerHandler : IRuleTriggerHandler
{
private const string EventName = "Usage exceeded";
- protected override Task CreateEnrichedEventAsync(Envelope @event)
+ public Type TriggerType => typeof(UsageTrigger);
+
+ public bool Handles(AppEvent appEvent)
+ {
+ return appEvent is AppUsageExceeded;
+ }
+
+ public async IAsyncEnumerable CreateEnrichedEventsAsync(Envelope @event, RuleContext context,
+ [EnumeratorCancellation] CancellationToken ct)
{
+ var usageEvent = (AppUsageExceeded)@event.Payload;
+
var result = new EnrichedUsageExceededEvent
{
- CallsCurrent = @event.Payload.CallsCurrent,
- CallsLimit = @event.Payload.CallsLimit,
+ CallsCurrent = usageEvent.CallsCurrent,
+ CallsLimit = usageEvent.CallsLimit,
Name = EventName
};
- return Task.FromResult(result);
+ await Task.Yield();
+
+ yield return result;
}
- protected override bool Trigger(EnrichedUsageExceededEvent @event, UsageTrigger trigger)
+ public bool Trigger(Envelope @event, RuleContext context)
{
- return @event.CallsLimit == trigger.Limit;
+ var trigger = (UsageTrigger)context.Rule.Trigger;
+
+ var usageEvent = (AppUsageExceeded)@event.Payload;
+
+ return usageEvent.CallsLimit >= trigger.Limit;
}
}
}
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs
index 58a4275a8..9c447addd 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs
@@ -5,6 +5,10 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Threading;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
@@ -18,10 +22,12 @@ using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Schemas
{
- public sealed class SchemaChangedTriggerHandler : RuleTriggerHandler
+ public sealed class SchemaChangedTriggerHandler : IRuleTriggerHandler
{
private readonly IScriptEngine scriptEngine;
+ public Type TriggerType => typeof(SchemaChangedTrigger);
+
public SchemaChangedTriggerHandler(IScriptEngine scriptEngine)
{
Guard.NotNull(scriptEngine, nameof(scriptEngine));
@@ -29,9 +35,15 @@ namespace Squidex.Domain.Apps.Entities.Schemas
this.scriptEngine = scriptEngine;
}
- protected override Task CreateEnrichedEventAsync(Envelope @event)
+ public bool Handles(AppEvent appEvent)
+ {
+ return appEvent is SchemaEvent;
+ }
+
+ public async IAsyncEnumerable CreateEnrichedEventsAsync(Envelope @event, RuleContext context,
+ [EnumeratorCancellation] CancellationToken ct)
{
- EnrichedSchemaEvent? result = new EnrichedSchemaEvent();
+ var result = new EnrichedSchemaEvent();
SimpleMapper.Map(@event.Payload, result);
@@ -57,20 +69,18 @@ namespace Squidex.Domain.Apps.Entities.Schemas
result.Type = EnrichedSchemaEventType.Deleted;
break;
default:
- result = null;
- break;
+ yield break;
}
- if (result != null)
- {
- result.Name = $"Schema{result.Type}";
- }
+ await Task.Yield();
- return Task.FromResult(result);
+ yield return result;
}
- protected override bool Trigger(EnrichedSchemaEvent @event, SchemaChangedTrigger trigger)
+ public bool Trigger(EnrichedEvent @event, RuleContext context)
{
+ var trigger = (SchemaChangedTrigger)context.Rule.Trigger;
+
if (string.IsNullOrWhiteSpace(trigger.Condition))
{
return true;
diff --git a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs
index 5bb2d0ea9..b498884d2 100644
--- a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs
+++ b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs
@@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Hosting;
@@ -47,34 +48,20 @@ namespace Squidex.Infrastructure.EventSourcing
var result = new List();
- await documentClient.QueryAsync(collectionUri, query, commit =>
+ await foreach (var commit in documentClient.QueryAsync(collectionUri, query, default))
{
- var eventStreamOffset = (int)commit.EventStreamOffset;
-
- var commitTimestamp = commit.Timestamp;
- var commitOffset = 0;
-
- foreach (var @event in commit.Events)
+ foreach (var storedEvent in commit.Filtered().Reverse())
{
- eventStreamOffset++;
+ result.Add(storedEvent);
- var eventData = @event.ToEventData();
- var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length);
-
- result.Add(new StoredEvent(streamName, eventToken, eventStreamOffset, eventData));
+ if (result.Count == count)
+ {
+ break;
+ }
}
-
- return Task.CompletedTask;
- });
-
- IEnumerable ordered = result.OrderBy(x => x.EventStreamNumber);
-
- if (result.Count > count)
- {
- ordered = ordered.Skip(result.Count - count);
}
- return ordered.ToList();
+ return result;
}
}
@@ -90,72 +77,89 @@ namespace Squidex.Infrastructure.EventSourcing
var result = new List();
- await documentClient.QueryAsync(collectionUri, query, commit =>
+ await foreach (var commit in documentClient.QueryAsync(collectionUri, query, default))
{
- var eventStreamOffset = (int)commit.EventStreamOffset;
-
- var commitTimestamp = commit.Timestamp;
- var commitOffset = 0;
-
- foreach (var @event in commit.Events)
+ foreach (var storedEvent in commit.Filtered().Reverse())
{
- 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));
- }
+ result.Add(storedEvent);
}
-
- return Task.CompletedTask;
- });
+ }
return result;
}
}
- public async Task QueryAsync(Func callback, string? streamFilter = null, string? position = null, CancellationToken ct = default)
+ public async IAsyncEnumerable QueryAllAsync( string? streamFilter = null, string? position = null, long take = long.MaxValue,
+ [EnumeratorCancellation] CancellationToken ct = default)
{
- Guard.NotNull(callback, nameof(callback));
-
ThrowIfDisposed();
+ if (take <= 0)
+ {
+ yield break;
+ }
+
StreamPosition lastPosition = position;
- var filterDefinition = FilterBuilder.CreateByFilter(streamFilter, lastPosition);
- var filterExpression = FilterBuilder.CreateExpression(null, null);
+ var filterDefinition = FilterBuilder.CreateByFilter(streamFilter, lastPosition, "ASC", take);
- using (Profiler.TraceMethod())
+ var taken = int.MaxValue;
+
+ await foreach (var commit in documentClient.QueryAsync(collectionUri, filterDefinition, ct: ct))
{
- await documentClient.QueryAsync(collectionUri, filterDefinition, async commit =>
+ if (taken == take)
{
- var eventStreamOffset = (int)commit.EventStreamOffset;
-
- var commitTimestamp = commit.Timestamp;
- var commitOffset = 0;
+ yield break;
+ }
- foreach (var @event in commit.Events)
+ foreach (var storedEvent in commit.Filtered(lastPosition))
+ {
+ if (taken == take)
{
- eventStreamOffset++;
+ yield break;
+ }
- if (commitOffset > lastPosition.CommitOffset || commitTimestamp > lastPosition.Timestamp)
- {
- var eventData = @event.ToEventData();
+ yield return storedEvent;
- if (filterExpression(eventData))
- {
- var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length);
+ taken++;
+ }
+ }
+ }
- await callback(new StoredEvent(commit.EventStream, eventToken, eventStreamOffset, eventData));
- }
- }
+ public async IAsyncEnumerable QueryAllReverseAsync(string? streamFilter = null, string? position = null, long take = long.MaxValue,
+ [EnumeratorCancellation] CancellationToken ct = default)
+ {
+ ThrowIfDisposed();
+
+ if (take <= 0)
+ {
+ yield break;
+ }
- commitOffset++;
+ StreamPosition lastPosition = position;
+
+ var filterDefinition = FilterBuilder.CreateByFilter(streamFilter, lastPosition, "DESC", take);
+
+ var taken = long.MaxValue;
+
+ await foreach (var commit in documentClient.QueryAsync(collectionUri, filterDefinition, ct: ct))
+ {
+ if (taken == take)
+ {
+ yield break;
+ }
+
+ foreach (var storedEvent in commit.Filtered(lastPosition))
+ {
+ if (taken == take)
+ {
+ yield break;
}
- }, ct);
+
+ yield return storedEvent;
+
+ taken++;
+ }
}
}
}
diff --git a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs
index e8329813e..68bf3d0d5 100644
--- a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs
+++ b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs
@@ -21,7 +21,7 @@ namespace Squidex.Infrastructure.EventSourcing
private const int MaxWriteAttempts = 20;
private const int MaxCommitSize = 10;
- public Task DeleteStreamAsync(string streamName)
+ public async Task DeleteStreamAsync(string streamName)
{
Guard.NotNullOrEmpty(streamName, nameof(streamName));
@@ -34,12 +34,12 @@ namespace Squidex.Infrastructure.EventSourcing
PartitionKey = new PartitionKey(streamName)
};
- return documentClient.QueryAsync(collectionUri, query, commit =>
+ await foreach (var commit in documentClient.QueryAsync(collectionUri, query))
{
var documentUri = UriFactory.CreateDocumentUri(DatabaseId, Constants.Collection, commit.Id.ToString());
- return documentClient.DeleteDocumentAsync(documentUri, deleteOptions);
- });
+ await documentClient.DeleteDocumentAsync(documentUri, deleteOptions);
+ }
}
public Task AppendAsync(Guid commitId, string streamName, ICollection events)
diff --git a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs
index 89f3f2b6f..f24d50f8c 100644
--- a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs
+++ b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs
@@ -7,7 +7,6 @@
using System.Collections.Generic;
using Microsoft.Azure.Documents;
-using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Infrastructure.EventSourcing
{
@@ -70,10 +69,10 @@ namespace Squidex.Infrastructure.EventSourcing
return new SqlQuerySpec(query, parameters);
}
- public static SqlQuerySpec ByStreamNameDesc(string streamName, long count)
+ public static SqlQuerySpec ByStreamNameDesc(string streamName, long take)
{
var query =
- $"SELECT TOP {count}* " +
+ $"SELECT TOP {take}* " +
$"FROM {Constants.Collection} e " +
$"WHERE " +
$" e.eventStream = @name " +
@@ -87,19 +86,7 @@ namespace Squidex.Infrastructure.EventSourcing
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)
+ public static SqlQuerySpec CreateByFilter(string? streamFilter, StreamPosition streamPosition, string sortOrder, long take)
{
var filters = new List();
@@ -108,23 +95,16 @@ namespace Squidex.Infrastructure.EventSourcing
filters.ForPosition(parameters, streamPosition);
filters.ForRegex(parameters, streamFilter);
- return BuildQuery(filters, parameters);
+ return BuildQuery(filters, parameters, sortOrder, take);
}
- private static SqlQuerySpec BuildQuery(IEnumerable filters, SqlParameterCollection parameters)
+ private static SqlQuerySpec BuildQuery(IEnumerable filters, SqlParameterCollection parameters, string sortOrder, long take)
{
- var query = $"SELECT * FROM {Constants.Collection} e WHERE {string.Join(" AND ", filters)} ORDER BY e.timestamp";
+ var query = $"SELECT TOP {take} * FROM {Constants.Collection} e WHERE {string.Join(" AND ", filters)} ORDER BY e.timestamp {sortOrder}";
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))
@@ -155,19 +135,5 @@ namespace Squidex.Infrastructure.EventSourcing
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
index 68d13782c..f5e074b55 100644
--- a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs
+++ b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs
@@ -6,7 +6,9 @@
// ==========================================================================
using System;
+using System.Collections.Generic;
using System.Linq;
+using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Documents;
@@ -39,29 +41,71 @@ namespace Squidex.Infrastructure.EventSourcing
return default!;
}
- public static Task QueryAsync(this DocumentClient documentClient, Uri collectionUri, SqlQuerySpec querySpec, Func handler, CancellationToken ct = default)
+ public static async IAsyncEnumerable QueryAsync(this DocumentClient documentClient, Uri collectionUri, SqlQuerySpec querySpec,
+ [EnumeratorCancellation] CancellationToken ct = default)
{
var query = documentClient.CreateDocumentQuery(collectionUri, querySpec, CrossPartition);
- return query.QueryAsync(handler, ct);
- }
-
- public static async Task QueryAsync(this IQueryable queryable, Func handler, CancellationToken ct = default)
- {
- var documentQuery = queryable.AsDocumentQuery();
+ var documentQuery = query.AsDocumentQuery();
using (documentQuery)
{
while (documentQuery.HasMoreResults && !ct.IsCancellationRequested)
{
- var items = await documentQuery.ExecuteNextAsync(ct);
+ var items = await documentQuery.ExecuteNextAsync(ct);
foreach (var item in items)
{
- await handler(item);
+ yield return item;
}
}
}
}
+
+ public static IEnumerable Filtered(this CosmosDbEventCommit commit, StreamPosition lastPosition)
+ {
+ var eventStreamOffset = 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();
+ var eventPosition = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length);
+
+ yield return new StoredEvent(commit.EventStream, eventPosition, eventStreamOffset, eventData);
+ }
+
+ commitOffset++;
+ }
+ }
+
+ public static IEnumerable Filtered(this CosmosDbEventCommit commit, long streamPosition = EtagVersion.Empty)
+ {
+ var eventStreamOffset = commit.EventStreamOffset;
+
+ var commitTimestamp = commit.Timestamp;
+ var commitOffset = 0;
+
+ foreach (var @event in commit.Events)
+ {
+ eventStreamOffset++;
+
+ if (eventStreamOffset >= streamPosition)
+ {
+ var eventData = @event.ToEventData();
+ var eventPosition = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length);
+
+ yield return new StoredEvent(commit.EventStream, eventPosition, eventStreamOffset, eventData);
+ }
+
+ commitOffset++;
+ }
+ }
}
}
diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs
index 88c640a08..929a2a23e 100644
--- a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs
+++ b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs
@@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using EventStore.ClientAPI;
@@ -26,7 +27,7 @@ namespace Squidex.Infrastructure.EventSourcing
private static readonly IReadOnlyList EmptyEvents = new List();
private readonly IEventStoreConnection connection;
private readonly IJsonSerializer serializer;
- private readonly string prefix;
+ private readonly string prefix = "squidex";
private readonly ProjectionClient projectionClient;
public GetEventStore(IEventStoreConnection connection, IJsonSerializer serializer, string prefix, string projectionHost)
@@ -37,9 +38,12 @@ namespace Squidex.Infrastructure.EventSourcing
this.connection = connection;
this.serializer = serializer;
- this.prefix = prefix.Trim(' ', '-').Or("squidex");
+ if (!string.IsNullOrWhiteSpace(prefix))
+ {
+ this.prefix = prefix.Trim(' ', '-');
+ }
- projectionClient = new ProjectionClient(connection, prefix, projectionHost);
+ projectionClient = new ProjectionClient(connection, this.prefix, projectionHost);
}
public async Task InitializeAsync(CancellationToken ct = default)
@@ -65,40 +69,40 @@ namespace Squidex.Infrastructure.EventSourcing
return new GetEventStoreSubscription(connection, subscriber, serializer, projectionClient, position, prefix, streamFilter);
}
- public async Task QueryAsync(Func callback, string? streamFilter = null, string? position = null, CancellationToken ct = default)
+ public async IAsyncEnumerable QueryAllAsync(string? streamFilter = null, string? position = null, long take = long.MaxValue,
+ [EnumeratorCancellation] CancellationToken ct = default)
{
- Guard.NotNull(callback, nameof(callback));
-
- using (Profiler.TraceMethod())
+ if (take <= 0)
{
- var streamName = await projectionClient.CreateProjectionAsync(streamFilter);
+ yield break;
+ }
- var sliceStart = ProjectionClient.ParsePosition(position);
+ var streamName = await projectionClient.CreateProjectionAsync(streamFilter);
- await QueryAsync(callback, streamName, sliceStart, ct);
+ var sliceStart = ProjectionClient.ParsePosition(position);
+
+ await foreach (var storedEvent in QueryReverseAsync(streamName, sliceStart, take, ct))
+ {
+ yield return storedEvent;
}
}
- private async Task QueryAsync(Func callback, string streamName, long sliceStart, CancellationToken ct = default)
+ public async IAsyncEnumerable QueryAllReverseAsync(string? streamFilter = null, string? position = null, long take = long.MaxValue,
+ [EnumeratorCancellation] CancellationToken ct = default)
{
- StreamEventsSlice currentSlice;
- do
+ if (take <= 0)
{
- currentSlice = await connection.ReadStreamEventsForwardAsync(streamName, sliceStart, ReadPageSize, true);
+ yield break;
+ }
- if (currentSlice.Status == SliceReadStatus.Success)
- {
- sliceStart = currentSlice.NextEventNumber;
+ var streamName = await projectionClient.CreateProjectionAsync(streamFilter);
- foreach (var resolved in currentSlice.Events)
- {
- var storedEvent = Formatter.Read(resolved, prefix, serializer);
+ var sliceStart = ProjectionClient.ParsePosition(position);
- await callback(storedEvent);
- }
- }
+ await foreach (var storedEvent in QueryAsync(streamName, sliceStart, take, ct))
+ {
+ yield return storedEvent;
}
- while (!currentSlice.IsEndOfStream && !ct.IsCancellationRequested);
}
public async Task> QueryLatestAsync(string streamName, int count)
@@ -114,35 +118,12 @@ namespace Squidex.Infrastructure.EventSourcing
{
var result = new List();
- var sliceStart = (long)StreamPosition.End;
-
- StreamEventsSlice currentSlice;
- do
- {
- currentSlice = await connection.ReadStreamEventsBackwardAsync(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);
-
- IEnumerable ordered = result.OrderBy(x => x.EventStreamNumber);
-
- if (result.Count > count)
+ await foreach (var storedEvent in QueryReverseAsync(streamName, StreamPosition.End, default))
{
- ordered = ordered.Skip(result.Count - count);
+ result.Add(storedEvent);
}
- return ordered.ToList();
+ return result.ToList();
}
}
@@ -154,29 +135,77 @@ namespace Squidex.Infrastructure.EventSourcing
{
var result = new List();
- var sliceStart = streamPosition >= 0 ? streamPosition : StreamPosition.Start;
+ await foreach (var storedEvent in QueryAsync(streamName, StreamPosition.End, default))
+ {
+ result.Add(storedEvent);
+ }
+
+ return result.ToList();
+ }
+ }
+
+ private async IAsyncEnumerable QueryAsync(string streamName, long sliceStart, long take = int.MaxValue,
+ [EnumeratorCancellation] CancellationToken ct = default)
+ {
+ var taken = take;
- StreamEventsSlice currentSlice;
- do
+ StreamEventsSlice currentSlice;
+ do
+ {
+ currentSlice = await connection.ReadStreamEventsForwardAsync(streamName, sliceStart, ReadPageSize, true);
+
+ if (currentSlice.Status == SliceReadStatus.Success)
{
- currentSlice = await connection.ReadStreamEventsForwardAsync(GetStreamName(streamName), sliceStart, ReadPageSize, true);
+ sliceStart = currentSlice.NextEventNumber;
- if (currentSlice.Status == SliceReadStatus.Success)
+ foreach (var resolved in currentSlice.Events)
{
- sliceStart = currentSlice.NextEventNumber;
+ var storedEvent = Formatter.Read(resolved, prefix, serializer);
- foreach (var resolved in currentSlice.Events)
- {
- var storedEvent = Formatter.Read(resolved, prefix, serializer);
+ yield return storedEvent;
- result.Add(storedEvent);
+ if (taken == take)
+ {
+ break;
}
+
+ taken++;
}
}
- while (!currentSlice.IsEndOfStream);
+ }
+ while (!currentSlice.IsEndOfStream && !ct.IsCancellationRequested && taken < take);
+ }
+
+ private async IAsyncEnumerable QueryReverseAsync(string streamName, long sliceStart, long take = int.MaxValue,
+ [EnumeratorCancellation] CancellationToken ct = default)
+ {
+ var taken = take;
+
+ StreamEventsSlice currentSlice;
+ do
+ {
+ currentSlice = await connection.ReadStreamEventsBackwardAsync(streamName, sliceStart, ReadPageSize, true);
+
+ if (currentSlice.Status == SliceReadStatus.Success)
+ {
+ sliceStart = currentSlice.NextEventNumber;
- return result;
+ foreach (var resolved in currentSlice.Events.OrderByDescending(x => x.Event.EventNumber))
+ {
+ var storedEvent = Formatter.Read(resolved, prefix, serializer);
+
+ yield return storedEvent;
+
+ if (taken == take)
+ {
+ break;
+ }
+
+ taken++;
+ }
+ }
}
+ while (!currentSlice.IsEndOfStream && !ct.IsCancellationRequested && taken < take);
}
public Task DeleteStreamAsync(string streamName)
diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs
index 81f588814..554ccb2bb 100644
--- a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs
+++ b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs
@@ -9,6 +9,7 @@ using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Net;
+using System.Net.Http;
using System.Net.Sockets;
using System.Threading.Tasks;
using EventStore.ClientAPI;
@@ -23,21 +24,21 @@ namespace Squidex.Infrastructure.EventSourcing
{
private readonly ConcurrentDictionary projections = new ConcurrentDictionary();
private readonly IEventStoreConnection connection;
- private readonly string prefix;
+ private readonly string projectionPrefix;
private readonly string projectionHost;
private ProjectionsManager projectionsManager;
- public ProjectionClient(IEventStoreConnection connection, string prefix, string projectionHost)
+ public ProjectionClient(IEventStoreConnection connection, string projectionPrefix, string projectionHost)
{
this.connection = connection;
- this.prefix = prefix;
+ this.projectionPrefix = projectionPrefix;
this.projectionHost = projectionHost;
}
private string CreateFilterProjectionName(string filter)
{
- return $"by-{prefix.Slugify()}-{filter.Slugify()}";
+ return $"by-{projectionPrefix.Slugify()}-{filter.Slugify()}";
}
public async Task CreateProjectionAsync(string? streamFilter = null)
@@ -50,7 +51,7 @@ namespace Squidex.Infrastructure.EventSourcing
$@"fromAll()
.when({{
$any: function (s, e) {{
- if (e.streamId.indexOf('{prefix}') === 0 && /{streamFilter}/.test(e.streamId.substring({prefix.Length + 1}))) {{
+ if (e.streamId.indexOf('{projectionPrefix}') === 0 && /{streamFilter}/.test(e.streamId.substring({projectionPrefix.Length + 1}))) {{
linkTo('{name}', e);
}}
}}
@@ -93,14 +94,33 @@ namespace Squidex.Infrastructure.EventSourcing
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
+ async Task ConnectToSchemaAsync(string schema)
{
+ projectionsManager =
+ new ProjectionsManager(
+ connection.Settings.Log, endpoint,
+ connection.Settings.OperationTimeout,
+ null,
+ schema);
+
await projectionsManager.ListAllAsync(connection.Settings.DefaultUserCredentials);
}
+
+ try
+ {
+ try
+ {
+ await ConnectToSchemaAsync("https");
+ }
+ catch (HttpRequestException)
+ {
+ await ConnectToSchemaAsync("http");
+ }
+ catch (AggregateException ex) when (ex.Flatten().InnerException is HttpRequestException)
+ {
+ await ConnectToSchemaAsync("http");
+ }
+ }
catch (Exception ex)
{
var error = new ConfigurationError($"GetEventStore cannot connect to event store projections: {projectionHost}.");
diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/Filtering.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/FilterExtensions.cs
similarity index 97%
rename from backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/Filtering.cs
rename to backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/FilterExtensions.cs
index 2c96a52be..da66bffb5 100644
--- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/Filtering.cs
+++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/FilterExtensions.cs
@@ -10,7 +10,7 @@ using MongoDB.Driver;
namespace Squidex.Infrastructure.EventSourcing
{
- internal static class Filtering
+ internal static class FilterExtensions
{
public static FilterDefinition ByPosition(StreamPosition streamPosition)
{
@@ -81,7 +81,7 @@ namespace Squidex.Infrastructure.EventSourcing
}
}
- public static IEnumerable Filtered(this MongoEventCommit commit, long streamPosition)
+ public static IEnumerable Filtered(this MongoEventCommit commit, long streamPosition = EtagVersion.Empty)
{
var eventStreamOffset = commit.EventStreamOffset;
diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStoreSubscription.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStoreSubscription.cs
index e72393b41..26ab00340 100644
--- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStoreSubscription.cs
+++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStoreSubscription.cs
@@ -114,7 +114,7 @@ namespace Squidex.Infrastructure.EventSourcing
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, stopToken.Token))
{
- await eventStore.QueryAsync(async storedEvent =>
+ await foreach (var storedEvent in eventStore.QueryAllAsync(streamFilter, position, ct: combined.Token))
{
var now = SystemClock.Instance.GetCurrentInstant();
@@ -130,7 +130,7 @@ namespace Squidex.Infrastructure.EventSourcing
lastRawPosition = storedEvent.EventPosition;
}
- }, streamFilter, position, combined.Token);
+ }
}
}
@@ -141,7 +141,7 @@ namespace Squidex.Infrastructure.EventSourcing
{
var result = new EmptyPipelineDefinition>();
- var byStream = Filtering.ByChangeInStream(streamFilter);
+ var byStream = FilterExtensions.ByChangeInStream(streamFilter);
if (byStream != null)
{
diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs
index 7eda87401..3dbd2605e 100644
--- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs
+++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs
@@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
@@ -23,17 +24,6 @@ namespace Squidex.Infrastructure.EventSourcing
{
private static readonly List EmptyEvents = new List();
- public Task CreateIndexAsync(string property)
- {
- Guard.NotNullOrEmpty(property, nameof(property));
-
- return Collection.Indexes.CreateOneAsync(
- new CreateIndexModel(
- Index
- .Ascending(CreateIndexPath(property))
- .Ascending(TimestampField)));
- }
-
public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string? streamFilter = null, string? position = null)
{
Guard.NotNull(subscriber, nameof(subscriber));
@@ -64,21 +54,9 @@ namespace Squidex.Infrastructure.EventSourcing
Filter.Eq(EventStreamField, streamName))
.Sort(Sort.Descending(TimestampField)).Limit(count).ToListAsync();
- var result = new List();
+ var result = commits.Select(x => x.Filtered()).Reverse().SelectMany(x => x).TakeLast(count).ToList();
- foreach (var commit in commits)
- {
- result.AddRange(commit.Filtered(long.MinValue));
- }
-
- IEnumerable ordered = result.OrderBy(x => x.EventStreamNumber);
-
- if (result.Count > count)
- {
- ordered = ordered.Skip(result.Count - count);
- }
-
- return ordered.ToList();
+ return result;
}
}
@@ -125,35 +103,95 @@ namespace Squidex.Infrastructure.EventSourcing
}
}
- public Task QueryAsync(Func callback, string? streamFilter = null, string? position = null, CancellationToken ct = default)
+ public async IAsyncEnumerable QueryAllReverseAsync(string? streamFilter = null, string? position = null, long take = long.MaxValue,
+ [EnumeratorCancellation] CancellationToken ct = default)
{
- Guard.NotNull(callback, nameof(callback));
+ if (take <= 0)
+ {
+ yield break;
+ }
StreamPosition lastPosition = position;
var filterDefinition = CreateFilter(streamFilter, lastPosition);
- return QueryAsync(callback, lastPosition, filterDefinition, ct);
+ var find =
+ Collection.Find(filterDefinition, options: Batching.Options)
+ .Limit((int)take).Sort(Sort.Descending(TimestampField));
+
+ var taken = 0;
+
+ using (var cursor = await find.ToCursorAsync(ct))
+ {
+ while (taken < take && await cursor.MoveNextAsync(ct))
+ {
+ foreach (var current in cursor.Current)
+ {
+ foreach (var @event in current.Filtered(position).Reverse())
+ {
+ yield return @event;
+
+ taken++;
+
+ if (taken == take)
+ {
+ break;
+ }
+ }
+
+ if (taken == take)
+ {
+ break;
+ }
+ }
+ }
+ }
}
- private async Task QueryAsync(Func callback, StreamPosition position, EventFilter filter, CancellationToken ct = default)
+ public async IAsyncEnumerable QueryAllAsync(string? streamFilter = null, string? position = null, long take = long.MaxValue,
+ [EnumeratorCancellation] CancellationToken ct = default)
{
- using (Profiler.TraceMethod())
+ StreamPosition lastPosition = position;
+
+ var filterDefinition = CreateFilter(streamFilter, lastPosition);
+
+ var find =
+ Collection.Find(filterDefinition)
+ .Limit((int)take).Sort(Sort.Ascending(TimestampField));
+
+ var taken = 0;
+
+ using (var cursor = await find.ToCursorAsync(ct))
{
- await Collection.Find(filter, options: Batching.Options).Sort(Sort.Ascending(TimestampField)).ForEachPipedAsync(async commit =>
+ while (taken < take && await cursor.MoveNextAsync(ct))
{
- foreach (var @event in commit.Filtered(position))
+ foreach (var current in cursor.Current)
{
- await callback(@event);
+ foreach (var @event in current.Filtered(position))
+ {
+ yield return @event;
+
+ taken++;
+
+ if (taken == take)
+ {
+ break;
+ }
+ }
+
+ if (taken == take)
+ {
+ break;
+ }
}
- }, ct);
+ }
}
}
private static EventFilter CreateFilter(string? streamFilter, StreamPosition streamPosition)
{
- var byPosition = Filtering.ByPosition(streamPosition);
- var byStream = Filtering.ByStream(streamFilter);
+ var byPosition = FilterExtensions.ByPosition(streamPosition);
+ var byStream = FilterExtensions.ByStream(streamFilter);
if (byStream != null)
{
@@ -162,10 +200,5 @@ namespace Squidex.Infrastructure.EventSourcing
return byPosition;
}
-
- private static string CreateIndexPath(string property)
- {
- return $"Events.Metadata.{property}";
- }
}
}
\ No newline at end of file
diff --git a/backend/src/Squidex.Infrastructure/CollectionExtensions.cs b/backend/src/Squidex.Infrastructure/CollectionExtensions.cs
index 545bd0179..eac71f6a6 100644
--- a/backend/src/Squidex.Infrastructure/CollectionExtensions.cs
+++ b/backend/src/Squidex.Infrastructure/CollectionExtensions.cs
@@ -8,14 +8,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Threading.Tasks;
-
-#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
namespace Squidex.Infrastructure
{
public static class CollectionExtensions
{
+ public static bool SetEquals(this IReadOnlyCollection source, IReadOnlyCollection other)
+ {
+ return source.Count == other.Count && source.Intersect(other).Count() == other.Count;
+ }
+
public static IEnumerable> Batch(this IEnumerable source, int size)
{
TSource[]? bucket = null;
@@ -48,34 +50,14 @@ namespace Squidex.Infrastructure
}
}
- public static async Task> ToListAsync(this IAsyncEnumerable source)
- {
- var result = new List();
-
- await foreach (var item in source)
- {
- result.Add(item);
- }
-
- return result;
- }
-
- public static async IAsyncEnumerable ToAsyncEnumerable(this IEnumerable source)
- {
- foreach (var item in source)
- {
- yield return item;
- }
- }
-
- public static bool SetEquals(this IReadOnlyCollection source, IReadOnlyCollection other)
+ public static bool SetEquals(this IReadOnlyCollection source, IReadOnlyCollection other, IEqualityComparer comparer)
{
- return source.Count == other.Count && source.Intersect(other).Count() == other.Count;
+ return source.Count == other.Count && source.Intersect(other, comparer).Count() == other.Count;
}
- public static bool SetEquals(this IReadOnlyCollection source, IReadOnlyCollection other, IEqualityComparer comparer)
+ public static IEnumerable Reverse(this IEnumerable source, bool reverse)
{
- return source.Count == other.Count && source.Intersect(other, comparer).Count() == other.Count;
+ return reverse ? source.Reverse() : source;
}
public static IResultList SortSet(this IResultList input, Func idProvider, IReadOnlyList ids) where T : class
diff --git a/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs b/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs
index 1cd68835b..22e506198 100644
--- a/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs
+++ b/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs
@@ -59,12 +59,12 @@ namespace Squidex.Infrastructure.Commands
await InsertManyAsync(store, async target =>
{
- await eventStore.QueryAsync(async storedEvent =>
+ await foreach (var storedEvent in eventStore.QueryAllAsync(filter, ct: ct))
{
var id = storedEvent.Data.Headers.AggregateId();
await target(id);
- }, filter, ct: ct);
+ }
}, ct);
}
diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/EventData.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EventData.cs
index 756d7ae63..0aaaf6d42 100644
--- a/backend/src/Squidex.Infrastructure/EventSourcing/EventData.cs
+++ b/backend/src/Squidex.Infrastructure/EventSourcing/EventData.cs
@@ -5,27 +5,9 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
+#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
+
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;
- }
- }
+ public sealed record EventData(string Type, EnvelopeHeaders Headers, string Payload);
}
\ No newline at end of file
diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs
index 776273f8a..62eb88fd5 100644
--- a/backend/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs
+++ b/backend/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs
@@ -14,11 +14,13 @@ namespace Squidex.Infrastructure.EventSourcing
{
public interface IEventStore
{
- Task> QueryLatestAsync(string streamName, int count);
+ Task> QueryLatestAsync(string streamName, int take = int.MaxValue);
Task> QueryAsync(string streamName, long streamPosition = 0);
- Task QueryAsync(Func callback, string? streamFilter = null, string? position = null, CancellationToken ct = default);
+ IAsyncEnumerable QueryAllReverseAsync(string? streamFilter = null, string? position = null, long take = long.MaxValue, CancellationToken ct = default);
+
+ IAsyncEnumerable QueryAllAsync(string? streamFilter = null, string? position = null, long take = long.MaxValue, CancellationToken ct = default);
Task AppendAsync(Guid commitId, string streamName, ICollection events);
@@ -42,7 +44,7 @@ namespace Squidex.Infrastructure.EventSourcing
foreach (var streamName in streamNames)
{
- result[streamName] = await QueryAsync(streamName);
+ result[streamName] = await QueryAsync(streamName, 0);
}
return result;
diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs b/backend/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs
index 3e0dc7006..4864762bc 100644
--- a/backend/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs
+++ b/backend/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs
@@ -28,12 +28,12 @@ namespace Squidex.Infrastructure.EventSourcing
{
try
{
- await eventStore.QueryAsync(async storedEvent =>
+ await foreach (var storedEvent in eventStore.QueryAllAsync(streamFilter, position, ct: ct))
{
await eventSubscriber.OnEventAsync(this, storedEvent);
position = storedEvent.EventPosition;
- }, streamFilter, position, ct);
+ }
}
catch (Exception ex)
{
diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs b/backend/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs
index 8ede858a2..71cb87540 100644
--- a/backend/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs
+++ b/backend/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs
@@ -5,30 +5,11 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
+#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
+
namespace Squidex.Infrastructure.EventSourcing
{
- public sealed class StoredEvent
+ public sealed record StoredEvent(string StreamName, string EventPosition, long EventStreamNumber, EventData Data)
{
- 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/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
index b6141614f..e7a06c120 100644
--- a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
+++ b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
@@ -32,7 +32,7 @@
-
+
diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs
index a37f4b393..391a1c562 100644
--- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs
+++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs
@@ -53,7 +53,7 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Converters
public RuleTriggerDto Visit(ContentChangedTriggerV2 trigger)
{
- var schemas = trigger.Schemas.Select(x => SimpleMapper.Map(x, new ContentChangedRuleTriggerSchemaDto())).ToArray();
+ var schemas = trigger.Schemas?.Select(ContentChangedRuleTriggerSchemaDto.FromTrigger).ToArray();
return new ContentChangedRuleTriggerDto { Schemas = schemas, HandleAll = trigger.HandleAll };
}
diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventDto.cs
new file mode 100644
index 000000000..df6d38f4e
--- /dev/null
+++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventDto.cs
@@ -0,0 +1,49 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.ComponentModel.DataAnnotations;
+using Squidex.Domain.Apps.Core.HandleRules;
+using Squidex.Domain.Apps.Entities.Rules.Runner;
+using Squidex.Infrastructure.Reflection;
+
+namespace Squidex.Areas.Api.Controllers.Rules.Models
+{
+ public sealed record SimulatedRuleEventDto
+ {
+ ///
+ /// The name of the event.
+ ///
+ [Required]
+ public string EventName { get; set; }
+
+ ///
+ /// The data for the action.
+ ///
+ public string? ActionName { get; set; }
+
+ ///
+ /// The name of the action.
+ ///
+ public string? ActionData { get; set; }
+
+ ///
+ /// The name of the event.
+ ///
+ public string? Error { get; set; }
+
+ ///
+ /// The reason why the event has been skipped.
+ ///
+ [Required]
+ public SkipReason SkipReason { get; set; }
+
+ public static SimulatedRuleEventDto FromSimulatedRuleEvent(SimulatedRuleEvent ruleEvent)
+ {
+ return SimpleMapper.Map(ruleEvent, new SimulatedRuleEventDto());
+ }
+ }
+}
diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventsDto.cs
new file mode 100644
index 000000000..c9cad3a73
--- /dev/null
+++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventsDto.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 Squidex.Domain.Apps.Entities.Rules;
+using Squidex.Domain.Apps.Entities.Rules.Runner;
+using Squidex.Infrastructure;
+using Squidex.Infrastructure.Validation;
+using Squidex.Web;
+
+namespace Squidex.Areas.Api.Controllers.Rules.Models
+{
+ public sealed class SimulatedRuleEventsDto : Resource
+ {
+ ///
+ /// The simulated rule events.
+ ///
+ [LocalizedRequired]
+ public SimulatedRuleEventDto[] Items { get; set; }
+
+ ///
+ /// The total number of simulated rule events.
+ ///
+ public long Total { get; set; }
+
+ public static SimulatedRuleEventsDto FromSimulatedRuleEvents(IList events)
+ {
+ var result = new SimulatedRuleEventsDto
+ {
+ Total = events.Count,
+ Items = events.Select(SimulatedRuleEventDto.FromSimulatedRuleEvent).ToArray()
+ };
+
+ return result;
+ }
+ }
+}
diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerDto.cs
index 7d8b22f89..52ef13473 100644
--- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerDto.cs
+++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerDto.cs
@@ -9,8 +9,6 @@ using System.Linq;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Infrastructure.Collections;
-using Squidex.Infrastructure.Reflection;
-using Squidex.Infrastructure.Validation;
namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers
{
@@ -19,8 +17,7 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers
///
/// The schema settings.
///
- [LocalizedRequired]
- public ContentChangedRuleTriggerSchemaDto[] Schemas { get; set; }
+ public ContentChangedRuleTriggerSchemaDto[]? Schemas { get; set; }
///
/// Determines whether the trigger should handle all content changes events.
@@ -29,7 +26,7 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers
public override RuleTrigger ToTrigger()
{
- var schemas = Schemas.Select(x => SimpleMapper.Map(x, new ContentChangedTriggerSchemaV2())).ToReadOnlyCollection();
+ var schemas = Schemas?.Select(x => x.ToTrigger()).ToReadOnlyCollection();
return new ContentChangedTriggerV2 { HandleAll = HandleAll, Schemas = schemas };
}
diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs
index 4c54f0f6b..7a575ed6f 100644
--- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs
+++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs
@@ -5,7 +5,9 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
+using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Infrastructure;
+using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers
{
@@ -20,5 +22,15 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers
/// Javascript condition when to trigger.
///
public string? Condition { get; set; }
+
+ public ContentChangedTriggerSchemaV2 ToTrigger()
+ {
+ return SimpleMapper.Map(this, new ContentChangedTriggerSchemaV2());
+ }
+
+ public static ContentChangedRuleTriggerSchemaDto FromTrigger(ContentChangedTriggerSchemaV2 trigger)
+ {
+ return SimpleMapper.Map(trigger, new ContentChangedRuleTriggerSchemaDto());
+ }
}
}
diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs
index e93e1d825..b15866212 100644
--- a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs
+++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs
@@ -15,6 +15,7 @@ using Microsoft.Net.Http.Headers;
using NodaTime;
using Squidex.Areas.Api.Controllers.Rules.Models;
using Squidex.Domain.Apps.Core.HandleRules;
+using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Domain.Apps.Entities.Rules.Commands;
using Squidex.Domain.Apps.Entities.Rules.Repositories;
@@ -32,13 +33,15 @@ namespace Squidex.Areas.Api.Controllers.Rules
[ApiExplorerSettings(GroupName = nameof(Rules))]
public sealed class RulesController : ApiController
{
+ private readonly EventJsonSchemaGenerator eventJsonSchemaGenerator;
+ private readonly IAppProvider appProvider;
+ private readonly IRuleEventRepository ruleEventsRepository;
private readonly IRuleQueryService ruleQuery;
private readonly IRuleRunnerService ruleRunnerService;
- private readonly IRuleEventRepository ruleEventsRepository;
- private readonly EventJsonSchemaGenerator eventJsonSchemaGenerator;
private readonly RuleRegistry ruleRegistry;
public RulesController(ICommandBus commandBus,
+ IAppProvider appProvider,
IRuleEventRepository ruleEventsRepository,
IRuleQueryService ruleQuery,
IRuleRunnerService ruleRunnerService,
@@ -46,6 +49,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
EventJsonSchemaGenerator eventJsonSchemaGenerator)
: base(commandBus)
{
+ this.appProvider = appProvider;
this.ruleEventsRepository = ruleEventsRepository;
this.ruleQuery = ruleQuery;
this.ruleRunnerService = ruleRunnerService;
@@ -260,6 +264,36 @@ namespace Squidex.Areas.Api.Controllers.Rules
return NoContent();
}
+ ///
+ /// Simulate a rule.
+ ///
+ /// The name of the app.
+ /// The id of the rule to simulate.
+ ///
+ /// 200 => Rule simulated.
+ /// 404 => Rule or app not found.
+ ///
+ [HttpGet]
+ [Route("apps/{app}/rules/{id}/simulate/")]
+ [ProducesResponseType(typeof(SimulatedRuleEventsDto), StatusCodes.Status200OK)]
+ [ApiPermissionOrAnonymous(Permissions.AppRulesEvents)]
+ [ApiCosts(5)]
+ public async Task Simulate(string app, DomainId id)
+ {
+ var rule = await appProvider.GetRuleAsync(AppId, id);
+
+ if (rule == null)
+ {
+ return NotFound();
+ }
+
+ var simulation = await ruleRunnerService.SimulateAsync(rule, HttpContext.RequestAborted);
+
+ var response = SimulatedRuleEventsDto.FromSimulatedRuleEvents(simulation);
+
+ return Ok(response);
+ }
+
///
/// Delete a rule.
///
diff --git a/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs b/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs
index e179710df..f0115b68c 100644
--- a/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs
+++ b/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs
@@ -6,7 +6,6 @@
// ==========================================================================
using System.Collections.Generic;
-using Squidex.Domain.Apps.Core.Apps;
namespace Squidex.Areas.Api.Controllers.UI
{
diff --git a/backend/src/Squidex/Config/Domain/EventSourcingServices.cs b/backend/src/Squidex/Config/Domain/EventSourcingServices.cs
index 6b0940af8..c40378a11 100644
--- a/backend/src/Squidex/Config/Domain/EventSourcingServices.cs
+++ b/backend/src/Squidex/Config/Domain/EventSourcingServices.cs
@@ -5,20 +5,14 @@
// 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.Commands;
-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
@@ -42,44 +36,6 @@ namespace Squidex.Config.Domain
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" });
}
});
diff --git a/backend/src/Squidex/Config/Domain/RuleServices.cs b/backend/src/Squidex/Config/Domain/RuleServices.cs
index 41e527982..e365a5d11 100644
--- a/backend/src/Squidex/Config/Domain/RuleServices.cs
+++ b/backend/src/Squidex/Config/Domain/RuleServices.cs
@@ -72,7 +72,7 @@ namespace Squidex.Config.Domain
services.AddSingletonAs()
.As();
- services.AddSingletonAs()
+ services.AddSingletonAs()
.As();
services.AddSingletonAs()
diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj
index c6fdd4bdd..5f6d69cce 100644
--- a/backend/src/Squidex/Squidex.csproj
+++ b/backend/src/Squidex/Squidex.csproj
@@ -24,8 +24,6 @@
-
-
@@ -47,6 +45,7 @@
+
diff --git a/backend/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json
index 9361f90e3..a4565523f 100644
--- a/backend/src/Squidex/appsettings.json
+++ b/backend/src/Squidex/appsettings.json
@@ -494,7 +494,7 @@
/*
* Define the type of the event store.
*
- * Supported: MongoDb, GetEventStore, CosmosDb
+ * Supported: MongoDb
*/
"type": "MongoDb",
"mongoDb": {
@@ -505,40 +505,6 @@
*/
"configuration": "mongodb://localhost",
- /*
- * The name of the event store database.
- */
- "database": "Squidex"
- },
- "getEventStore": {
- /*
- * The connection string to your EventStore.
- *
- * Read Mode: http://docs.geteventstore.com/dotnet-api/4.0.0/connecting-to-a-server/
- */
- "configuration": "ConnectTo=tcp://admin:changeit@localhost:1113; HeartBeatTimeout=500; MaxReconnections=-1",
-
- /*
- * The host name of your EventStore where projection requests will be sent to.
- */
- "projectionHost": "localhost",
-
- /*
- * Prefix for all streams and projections (for multiple installations).
- */
- "prefix": "squidex"
- },
- "cosmosDb": {
- /*
- * The connection string to your CosmosDB instance.
- */
- "configuration": "https://localhost:8081",
-
- /*
- * The primary access key.
- */
- "masterKey": "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
-
/*
* The name of the event store database.
*/
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
index c3cb1999a..e46647491 100644
--- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs
+++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs
@@ -93,6 +93,57 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
eventEnricher, TestUtils.DefaultSerializer, clock, log, typeNameRegistry);
}
+ [Fact]
+ public void Should_calculate_event_name_from_trigger_handler()
+ {
+ var @event = new ContentCreated();
+
+ A.CallTo(() => ruleTriggerHandler.Handles(@event))
+ .Returns(true);
+
+ A.CallTo(() => ruleTriggerHandler.GetName(@event))
+ .Returns("custom-name");
+
+ var name = sut.GetName(@event);
+
+ Assert.Equal("custom-name", name);
+ }
+
+ [Fact]
+ public void Should_calculate_default_name_if_trigger_handler_returns_no_name()
+ {
+ var @event = new ContentCreated();
+
+ A.CallTo(() => ruleTriggerHandler.Handles(@event))
+ .Returns(true);
+
+ A.CallTo(() => ruleTriggerHandler.GetName(@event))
+ .Returns(null);
+
+ var name = sut.GetName(@event);
+
+ Assert.Equal("ContentCreated", name);
+
+ A.CallTo(() => ruleTriggerHandler.GetName(@event))
+ .MustHaveHappened();
+ }
+
+ [Fact]
+ public void Should_calculate_default_name_if_trigger_handler_cannot_not_handle_event()
+ {
+ var @event = new ContentCreated();
+
+ A.CallTo(() => ruleTriggerHandler.Handles(@event))
+ .Returns(false);
+
+ var name = sut.GetName(@event);
+
+ Assert.Equal("ContentCreated", name);
+
+ A.CallTo(() => ruleTriggerHandler.GetName(@event))
+ .MustNotHaveHappened();
+ }
+
[Fact]
public void Should_not_run_from_snapshots_if_no_trigger_handler_registered()
{
@@ -107,7 +158,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents)
.Returns(false);
- var result = sut.CanCreateSnapshotEvents(ValidRule());
+ var result = sut.CanCreateSnapshotEvents(Rule());
Assert.False(result);
}
@@ -118,7 +169,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents)
.Returns(true);
- var result = sut.CanCreateSnapshotEvents(ValidRule());
+ var result = sut.CanCreateSnapshotEvents(Rule());
Assert.True(result);
}
@@ -129,11 +180,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents)
.Returns(false);
- var jobs = await sut.CreateSnapshotJobsAsync(ValidRule(), ruleId, appId.Id).ToListAsync();
+ var jobs = await sut.CreateSnapshotJobsAsync(Rule()).ToListAsync();
Assert.Empty(jobs);
- A.CallTo(() => ruleTriggerHandler.CreateSnapshotEvents(A._, A._))
+ A.CallTo(() => ruleTriggerHandler.CreateSnapshotEventsAsync(A._, A._))
.MustNotHaveHappened();
}
@@ -143,11 +194,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents)
.Returns(true);
- var jobs = await sut.CreateSnapshotJobsAsync(ValidRule().Disable(), ruleId, appId.Id).ToListAsync();
+ var jobs = await sut.CreateSnapshotJobsAsync(Rule(disable: true)).ToListAsync();
Assert.Empty(jobs);
- A.CallTo(() => ruleTriggerHandler.CreateSnapshotEvents(A._, A._))
+ A.CallTo(() => ruleTriggerHandler.CreateSnapshotEventsAsync(A._, A._))
.MustNotHaveHappened();
}
@@ -157,11 +208,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents)
.Returns(true);
- var jobs = await sut.CreateSnapshotJobsAsync(RuleInvalidTrigger(), ruleId, appId.Id).ToListAsync();
+ var jobs = await sut.CreateSnapshotJobsAsync(RuleInvalidTrigger()).ToListAsync();
Assert.Empty(jobs);
- A.CallTo(() => ruleTriggerHandler.CreateSnapshotEvents(A._, A._))
+ A.CallTo(() => ruleTriggerHandler.CreateSnapshotEventsAsync(A._, A