From 09345924e4ca5a9c4520aac3525f0d1a91c338e1 Mon Sep 17 00:00:00 2001 From: James Maes Date: Mon, 12 Oct 2026 18:23:15 -0500 Subject: [PATCH] chore!: remove the 0.x webhooks sources and tests The owner approved removing the old code. Deletes the 0.x engine (actions/, model/, tables/, the old processes and registry plugins) and its tests, which 1.0 replaced, and drops the TEMPORARY compiler and Checkstyle exclusions from pom.xml and the note in CLAUDE.md/AGENTS.md. ./mvnw clean verify: 144 tests, 0 failures, Checkstyle clean, JaCoCo gate met. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 6 - CLAUDE.md | 6 - pom.xml | 32 - ...tionHelperMemoizationsTableCustomizer.java | 60 -- .../DefaultWebhookEventTypeCustomizer.java | 31 - .../actions/FireAdHocWebhookEvent.java | 89 -- ...irePostInsertOrUpdateWebhookEventUtil.java | 138 --- .../actions/FirePostInsertWebhookEvent.java | 143 --- .../actions/FirePostUpdateWebhookEvent.java | 177 ---- .../webhooks/actions/WebhookEventBuilder.java | 152 --- .../webhooks/actions/WebhookEventSender.java | 417 --------- .../WebhookEventTypeCustomizerInterface.java | 92 -- .../actions/WebhookHealthManager.java | 114 --- .../actions/WebhookPostException.java | 50 - .../actions/WebhookRateLimitException.java | 37 - .../actions/WebhookSubscriptionsHelper.java | 259 ------ .../qbits/webhooks/model/Webhook.java | 538 ----------- .../webhooks/model/WebhookActiveStatus.java | 127 --- .../qbits/webhooks/model/WebhookEvent.java | 557 ----------- .../webhooks/model/WebhookEventCategory.java | 196 ---- .../webhooks/model/WebhookEventContent.java | 235 ----- .../webhooks/model/WebhookEventSendLog.java | 431 --------- .../webhooks/model/WebhookEventStatus.java | 129 --- .../webhooks/model/WebhookHealthStatus.java | 127 --- .../webhooks/model/WebhookSubscription.java | 471 ---------- .../webhooks/model/WebhooksActionFlags.java | 37 - ...eWebhookHealthProcessMetaDataProducer.java | 74 -- .../ManageWebhookHealthTransformStep.java | 151 --- .../SendTestEventToWebhookLoadStep.java | 170 ---- ...EventToWebhookProcessMetaDataProducer.java | 86 -- .../SendWebhookEventExtractStep.java | 129 --- .../processes/SendWebhookEventLoadStep.java | 259 ------ ...ndWebhookEventProcessMetaDataProducer.java | 81 -- .../SendWebhookEventTransformStep.java | 312 ------- .../SyncWebhookScheduledJobProcess.java | 113 --- .../WebhooksRegistryEnricherPlugin.java | 40 - .../WebhooksRegistryValidatorPlugin.java | 41 - .../tables/WebhookEventTableCustomizer.java | 47 - .../kingsrook/qbits/webhooks/BaseTest.java | 244 ----- .../webhooks/WebhooksQBitConfigTest.java | 190 ---- .../webhooks/WebhooksTestApplication.java | 199 ---- .../webhooks/WebhooksTestJavalinServer.java | 66 -- .../WebhooksTestReceiverApplication.java | 143 --- .../WebhooksTestReceiverJavalinServer.java | 65 -- .../actions/FireAdHocWebhookEventTest.java | 118 --- .../FirePostInsertWebhookEventTest.java | 223 ----- .../FirePostUpdateWebhookEventTest.java | 306 ------ .../actions/WebhookEventSenderTest.java | 880 ------------------ ...bhookEventTypeCustomizerInterfaceTest.java | 65 -- .../actions/WebhookHealthManagerTest.java | 133 --- .../WebhookSubscriptionsHelperTest.java | 98 -- .../ManageWebhookHealthTransformStepTest.java | 117 --- .../SendTestEventToWebhookLoadStepTest.java | 149 --- .../SendWebhookEventExtractStepTest.java | 235 ----- .../SendWebhookEventLoadStepTest.java | 280 ------ .../SendWebhookEventTransformStepTest.java | 149 --- ...ksRegistryValidationAndEnrichmentTest.java | 233 ----- 57 files changed, 10047 deletions(-) delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/actions/ClearWebhookSubscriptionHelperMemoizationsTableCustomizer.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/actions/DefaultWebhookEventTypeCustomizer.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/actions/FireAdHocWebhookEvent.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/actions/FirePostInsertOrUpdateWebhookEventUtil.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/actions/FirePostInsertWebhookEvent.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/actions/FirePostUpdateWebhookEvent.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookEventBuilder.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookEventSender.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookEventTypeCustomizerInterface.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookHealthManager.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookPostException.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookRateLimitException.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookSubscriptionsHelper.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/model/Webhook.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/model/WebhookActiveStatus.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/model/WebhookEvent.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/model/WebhookEventCategory.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/model/WebhookEventContent.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/model/WebhookEventSendLog.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/model/WebhookEventStatus.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/model/WebhookHealthStatus.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/model/WebhookSubscription.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/model/WebhooksActionFlags.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/processes/ManageWebhookHealthProcessMetaDataProducer.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/processes/ManageWebhookHealthTransformStep.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/processes/SendTestEventToWebhookLoadStep.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/processes/SendTestEventToWebhookProcessMetaDataProducer.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventExtractStep.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventLoadStep.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventProcessMetaDataProducer.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventTransformStep.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/processes/SyncWebhookScheduledJobProcess.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/registry/WebhooksRegistryEnricherPlugin.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/registry/WebhooksRegistryValidatorPlugin.java delete mode 100644 src/main/java/com/kingsrook/qbits/webhooks/tables/WebhookEventTableCustomizer.java delete mode 100644 src/test/java/com/kingsrook/qbits/webhooks/BaseTest.java delete mode 100644 src/test/java/com/kingsrook/qbits/webhooks/WebhooksQBitConfigTest.java delete mode 100644 src/test/java/com/kingsrook/qbits/webhooks/WebhooksTestApplication.java delete mode 100644 src/test/java/com/kingsrook/qbits/webhooks/WebhooksTestJavalinServer.java delete mode 100644 src/test/java/com/kingsrook/qbits/webhooks/WebhooksTestReceiverApplication.java delete mode 100644 src/test/java/com/kingsrook/qbits/webhooks/WebhooksTestReceiverJavalinServer.java delete mode 100644 src/test/java/com/kingsrook/qbits/webhooks/actions/FireAdHocWebhookEventTest.java delete mode 100644 src/test/java/com/kingsrook/qbits/webhooks/actions/FirePostInsertWebhookEventTest.java delete mode 100644 src/test/java/com/kingsrook/qbits/webhooks/actions/FirePostUpdateWebhookEventTest.java delete mode 100644 src/test/java/com/kingsrook/qbits/webhooks/actions/WebhookEventSenderTest.java delete mode 100644 src/test/java/com/kingsrook/qbits/webhooks/actions/WebhookEventTypeCustomizerInterfaceTest.java delete mode 100644 src/test/java/com/kingsrook/qbits/webhooks/actions/WebhookHealthManagerTest.java delete mode 100644 src/test/java/com/kingsrook/qbits/webhooks/actions/WebhookSubscriptionsHelperTest.java delete mode 100644 src/test/java/com/kingsrook/qbits/webhooks/processes/ManageWebhookHealthTransformStepTest.java delete mode 100644 src/test/java/com/kingsrook/qbits/webhooks/processes/SendTestEventToWebhookLoadStepTest.java delete mode 100644 src/test/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventExtractStepTest.java delete mode 100644 src/test/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventLoadStepTest.java delete mode 100644 src/test/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventTransformStepTest.java delete mode 100644 src/test/java/com/kingsrook/qbits/webhooks/registry/WebhooksRegistryValidationAndEnrichmentTest.java diff --git a/AGENTS.md b/AGENTS.md index 0612561..3e4e33f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,12 +52,6 @@ Design: `docs/DESIGN.md` (from QRun-IO/qqq#1105). Inbound webhooks are phase 2 ( 3-space indent, braces on the next line, `return (x);`, flower-box Javadoc on every class and method, fluent `withX()` setters. -## Temporary (until the owner approves removing them) - -- The 0.x sources (`actions/`, `model/`, `tables/`, the old `processes/*`, `registry/*Plugin`) are still in the tree. -- `pom.xml` excludes them from compile, test-compile and Checkstyle. -- Delete them and the excludes together. - ## Release - CircleCI qqq-orb 0.6.8. diff --git a/CLAUDE.md b/CLAUDE.md index 7a16a7b..ad4e93f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,12 +52,6 @@ Design: `docs/DESIGN.md` (from QRun-IO/qqq#1105). Inbound webhooks are phase 2 ( 3-space indent, braces on the next line, `return (x);`, flower-box Javadoc on every class and method, fluent `withX()` setters. -## Temporary (until the owner approves removing them) - -- The 0.x sources (`actions/`, `model/`, `tables/`, the old `processes/*`, `registry/*Plugin`) are still in the tree. -- `pom.xml` excludes them from compile, test-compile and Checkstyle. -- Delete them and the excludes together. - ## Release - CircleCI qqq-orb 0.6.8. diff --git a/pom.xml b/pom.xml index 2fb7bd5..b625d5b 100644 --- a/pom.xml +++ b/pom.xml @@ -128,36 +128,6 @@ - - - org.apache.maven.plugins - maven-compiler-plugin - - - com/kingsrook/qbits/webhooks/actions/** - com/kingsrook/qbits/webhooks/model/** - com/kingsrook/qbits/webhooks/tables/** - com/kingsrook/qbits/webhooks/registry/WebhooksRegistryEnricherPlugin.java - com/kingsrook/qbits/webhooks/registry/WebhooksRegistryValidatorPlugin.java - com/kingsrook/qbits/webhooks/processes/ManageWebhookHealth*.java - com/kingsrook/qbits/webhooks/processes/SendTestEventToWebhook*.java - com/kingsrook/qbits/webhooks/processes/SendWebhookEvent*.java - com/kingsrook/qbits/webhooks/processes/SyncWebhookScheduledJobProcess.java - - - com/kingsrook/qbits/webhooks/BaseTest.java - com/kingsrook/qbits/webhooks/WebhooksQBitConfigTest.java - com/kingsrook/qbits/webhooks/WebhooksTestApplication.java - com/kingsrook/qbits/webhooks/WebhooksTestJavalinServer.java - com/kingsrook/qbits/webhooks/WebhooksTestReceiver*.java - com/kingsrook/qbits/webhooks/actions/** - com/kingsrook/qbits/webhooks/processes/ManageWebhookHealthTransformStepTest.java - com/kingsrook/qbits/webhooks/processes/SendTestEventToWebhookLoadStepTest.java - com/kingsrook/qbits/webhooks/processes/SendWebhookEvent*Test.java - com/kingsrook/qbits/webhooks/registry/WebhooksRegistryValidationAndEnrichmentTest.java - - - org.apache.maven.plugins maven-checkstyle-plugin @@ -178,8 +148,6 @@ true warning false - - com/kingsrook/qbits/webhooks/actions/**,com/kingsrook/qbits/webhooks/model/**,com/kingsrook/qbits/webhooks/tables/**,com/kingsrook/qbits/webhooks/registry/WebhooksRegistry*Plugin.java,com/kingsrook/qbits/webhooks/processes/ManageWebhookHealth*.java,com/kingsrook/qbits/webhooks/processes/SendTestEventToWebhook*.java,com/kingsrook/qbits/webhooks/processes/SendWebhookEvent*.java,com/kingsrook/qbits/webhooks/processes/SyncWebhookScheduledJobProcess.java diff --git a/src/main/java/com/kingsrook/qbits/webhooks/actions/ClearWebhookSubscriptionHelperMemoizationsTableCustomizer.java b/src/main/java/com/kingsrook/qbits/webhooks/actions/ClearWebhookSubscriptionHelperMemoizationsTableCustomizer.java deleted file mode 100644 index 9fb9531..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/actions/ClearWebhookSubscriptionHelperMemoizationsTableCustomizer.java +++ /dev/null @@ -1,60 +0,0 @@ -/* -- a - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.actions; - - -import java.util.List; -import java.util.Optional; -import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; -import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; -import com.kingsrook.qqq.backend.core.model.data.QRecord; - - -/******************************************************************************* - ** table customizer that clears the memoizations in the WebhookSubscriptionsHelper - *******************************************************************************/ -public class ClearWebhookSubscriptionHelperMemoizationsTableCustomizer implements TableCustomizerInterface -{ - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public List postInsertOrUpdate(AbstractActionInput input, List records, Optional> oldRecordList) throws QException - { - WebhookSubscriptionsHelper.clearMemoizations();; - return (records); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public List postDelete(DeleteInput deleteInput, List records) throws QException - { - WebhookSubscriptionsHelper.clearMemoizations();; - return (records); - } -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/actions/DefaultWebhookEventTypeCustomizer.java b/src/main/java/com/kingsrook/qbits/webhooks/actions/DefaultWebhookEventTypeCustomizer.java deleted file mode 100644 index 9003054..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/actions/DefaultWebhookEventTypeCustomizer.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.actions; - - -/******************************************************************************* - ** Default implementation of WebhookEventTypeCustomizerInterface - just lets - ** all the default methods from the interface run. - *******************************************************************************/ -public class DefaultWebhookEventTypeCustomizer implements WebhookEventTypeCustomizerInterface -{ - -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/actions/FireAdHocWebhookEvent.java b/src/main/java/com/kingsrook/qbits/webhooks/actions/FireAdHocWebhookEvent.java deleted file mode 100644 index 41d8958..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/actions/FireAdHocWebhookEvent.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.actions; - - -import java.util.List; -import com.kingsrook.qbits.webhooks.model.WebhookEventCategory; -import com.kingsrook.qbits.webhooks.registry.WebhookEventType; -import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.logging.QLogger; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.utils.CollectionUtils; - - -/******************************************************************************* - * fire an ad-hoc webhook event - e.g., one not tied to a basic table insert/ - * update/delete - *******************************************************************************/ -public class FireAdHocWebhookEvent -{ - private static final QLogger LOG = QLogger.getLogger(FireAdHocWebhookEvent.class); - - - - /*************************************************************************** - * fire the event - creating webhookEvent records for any non-disabled - * subscriptions that match it. - * - * @param eventTypeName must be a registered event type name in the instance. - * @param tableName table that the key-record is from, which will be used as - * part of the subscription-matching - * @param keyRecord record that the event is related to - again may be used - * in subscription-matching (e.g., based on security fields - * @param transaction in case an update to the key record was made on a - * transaction, the events will be created in the same one. - ***************************************************************************/ - public void fire(String eventTypeName, String tableName, QRecord keyRecord, QBackendTransaction transaction) throws QException - { - List webhookEventTypes = WebhookSubscriptionsHelper.getWebhookEventTypesToConsiderFiringEventsFor(WebhookEventCategory.Kind.AD_HOC, tableName); - if(CollectionUtils.nullSafeIsEmpty(webhookEventTypes)) - { - return; - } - - WebhookEventBuilder webhookEventBuilder = null; - for(WebhookEventType webhookEventType : webhookEventTypes) - { - ////////////////////////////// - // filter by the event type // - ////////////////////////////// - if(!webhookEventType.getName().equals(eventTypeName)) - { - continue; - } - - if(keyRecord.getTableName() == null) - { - keyRecord.setTableName(tableName); - } - - webhookEventBuilder = FirePostInsertOrUpdateWebhookEventUtil.processSubscriptions(tableName, webhookEventType, keyRecord, webhookEventBuilder, transaction); - } - - if(webhookEventBuilder != null) - { - webhookEventBuilder.storeWebhookEvents(transaction); - } - } - -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/actions/FirePostInsertOrUpdateWebhookEventUtil.java b/src/main/java/com/kingsrook/qbits/webhooks/actions/FirePostInsertOrUpdateWebhookEventUtil.java deleted file mode 100644 index 27e8719..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/actions/FirePostInsertOrUpdateWebhookEventUtil.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.actions; - - -import java.io.Serializable; -import java.util.Collection; -import java.util.List; -import java.util.Objects; -import com.kingsrook.qbits.webhooks.model.WebhookSubscription; -import com.kingsrook.qbits.webhooks.registry.WebhookEventType; -import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; -import com.kingsrook.qqq.backend.core.context.QContext; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.logging.QLogger; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; -import com.kingsrook.qqq.backend.core.utils.CollectionUtils; -import com.kingsrook.qqq.backend.core.utils.StringUtils; -import com.kingsrook.qqq.backend.core.utils.ValueUtils; -import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; - - -/******************************************************************************* - ** utility for the fire post insert/update events - *******************************************************************************/ -public abstract class FirePostInsertOrUpdateWebhookEventUtil -{ - private static final QLogger LOG = QLogger.getLogger(FirePostInsertOrUpdateWebhookEventUtil.class); - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static WebhookEventBuilder processSubscriptions(String tableName, WebhookEventType webhookEventType, QRecord record, WebhookEventBuilder webhookEventBuilder, QBackendTransaction transaction) - { - List webhookSubscriptions = CollectionUtils.nonNullList(WebhookSubscriptionsHelper.getSubscriptionsForEventType(webhookEventType)); - for(WebhookSubscription webhookSubscription : webhookSubscriptions) - { - try - { - if(WebhookSubscriptionsHelper.doesRecordMatchSubscription(webhookEventType, webhookSubscription, record, transaction)) - { - LOG.info("Building webhook event", logPair("tableName", tableName), logPair("recordId", record.getValue("id")), logPair("webhookEventType", () -> webhookEventType.getName()), logPair("webhookSubscriptionId", webhookSubscription.getId())); - webhookEventBuilder = Objects.requireNonNullElseGet(webhookEventBuilder, WebhookEventBuilder::new); - webhookEventBuilder.addWebhookEvent(webhookSubscription, record); - } - } - catch(QException e) - { - LOG.warn("Error processing webhookSubscription", e, logPair("tableName", tableName), logPair("recordId", record.getValue("id")), logPair("webhookEventType", () -> webhookEventType.getName()), logPair("webhookSubscriptionId", webhookSubscription.getId())); - } - } - return webhookEventBuilder; - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static QFieldMetaData getQFieldMetaData(WebhookEventType webhookEventType) - { - QFieldMetaData field = null; - if(StringUtils.hasContent(webhookEventType.getFieldName())) - { - field = QContext.getQInstance().getTable(webhookEventType.getTableName()).getFields().get(webhookEventType.getFieldName()); - if(field == null) - { - LOG.warn("No field found for webhook event type", logPair("webhookEventType", webhookEventType), logPair("tableName", webhookEventType.getTableName()), logPair("fieldName", webhookEventType.getFieldName())); - } - } - return field; - } - - - - /*************************************************************************** - * - ***************************************************************************/ - public static Serializable getValue(QRecord record, QFieldMetaData field) - { - if(field == null) - { - return (null); - } - - return ValueUtils.getValueAsFieldType(field.getType(), record.getValue(field.getName())); - } - - - /*************************************************************************** - ** - ***************************************************************************/ - public static boolean doesNewValueMatchEventValue(WebhookEventType webhookEventType, QFieldMetaData field, Serializable newValue) - { - Serializable eventValue = webhookEventType.getValue(); - if(eventValue instanceof Collection collection) - { - for(Object o : collection) - { - Serializable eventSubValueAsFieldType = ValueUtils.getValueAsFieldType(field.getType(), o); - if(Objects.equals(eventSubValueAsFieldType, newValue)) - { - return true; - } - } - } - else - { - Serializable eventValueAsFieldType = ValueUtils.getValueAsFieldType(field.getType(), eventValue); - if(Objects.equals(eventValueAsFieldType, newValue)) - { - return true; - } - } - return false; - } -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/actions/FirePostInsertWebhookEvent.java b/src/main/java/com/kingsrook/qbits/webhooks/actions/FirePostInsertWebhookEvent.java deleted file mode 100644 index bbe2dfe..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/actions/FirePostInsertWebhookEvent.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.actions; - - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; -import com.kingsrook.qbits.webhooks.model.WebhookEventCategory; -import com.kingsrook.qbits.webhooks.model.WebhooksActionFlags; -import com.kingsrook.qbits.webhooks.registry.WebhookEventType; -import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.logging.QLogger; -import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; -import com.kingsrook.qqq.backend.core.utils.CollectionUtils; -import com.kingsrook.qqq.backend.core.utils.ValueUtils; -import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; - - -/******************************************************************************* - ** post-insert table customizer to fire webhook events - *******************************************************************************/ -public class FirePostInsertWebhookEvent implements TableCustomizerInterface -{ - private static final QLogger LOG = QLogger.getLogger(FirePostInsertWebhookEvent.class); - - - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public List postInsert(InsertInput insertInput, List records) throws QException - { - if(insertInput.hasFlag(WebhooksActionFlags.OMIT_WEBHOOKS)) - { - LOG.debug("Requested to omit webhooks after inert; returning early.", logPair("tableName", insertInput.getTableName())); - return (records); - } - - List insertWebhookEventTypes = WebhookSubscriptionsHelper.getWebhookEventTypesToConsiderFiringEventsFor(WebhookEventCategory.Kind.INSERT, insertInput.getTableName()); - List storeWebhookEventTypes = WebhookSubscriptionsHelper.getWebhookEventTypesToConsiderFiringEventsFor(WebhookEventCategory.Kind.STORE, insertInput.getTableName()); - if(CollectionUtils.nullSafeIsEmpty(insertWebhookEventTypes) && CollectionUtils.nullSafeIsEmpty(storeWebhookEventTypes)) - { - return (records); - } - - List webhookEventTypes = new ArrayList<>(); - CollectionUtils.addAllIfNotNull(webhookEventTypes, insertWebhookEventTypes); - CollectionUtils.addAllIfNotNull(webhookEventTypes, storeWebhookEventTypes); - - WebhookEventBuilder webhookEventBuilder = null; - for(WebhookEventType webhookEventType : webhookEventTypes) - { - QFieldMetaData field = FirePostInsertOrUpdateWebhookEventUtil.getQFieldMetaData(webhookEventType); - for(QRecord record : records) - { - if(CollectionUtils.nullSafeHasContents(record.getErrors())) - { - continue; - } - - if(record.getTableName() == null) - { - record.setTableName(insertInput.getTableName()); - } - - if(doesRecordMatchWebhookEventType(record, webhookEventType, field)) - { - webhookEventBuilder = FirePostInsertOrUpdateWebhookEventUtil.processSubscriptions(insertInput.getTableName(), webhookEventType, record, webhookEventBuilder, insertInput.getTransaction()); - } - } - } - - if(webhookEventBuilder != null) - { - webhookEventBuilder.storeWebhookEvents(insertInput.getTransaction()); - } - - return records; - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - boolean doesRecordMatchWebhookEventType(QRecord record, WebhookEventType webhookEventType, QFieldMetaData field) - { - switch(webhookEventType.getCategory()) - { - case INSERT, STORE -> - { - return (true); - } - case INSERT_WITH_FIELD, STORE_WITH_FIELD -> - { - Serializable value = FirePostInsertOrUpdateWebhookEventUtil.getValue(record, field); - if(value != null && !"".equals(ValueUtils.getValueAsString(value))) - { - return (true); - } - } - case INSERT_WITH_VALUE, STORE_WITH_VALUE -> - { - Serializable value = FirePostInsertOrUpdateWebhookEventUtil.getValue(record, field); - - if(FirePostInsertOrUpdateWebhookEventUtil.doesNewValueMatchEventValue(webhookEventType, field, value)) - { - return (true); - } - } - default -> - { - return (false); - } - } - - return (false); - } - -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/actions/FirePostUpdateWebhookEvent.java b/src/main/java/com/kingsrook/qbits/webhooks/actions/FirePostUpdateWebhookEvent.java deleted file mode 100644 index c7158fe..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/actions/FirePostUpdateWebhookEvent.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.actions; - - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import com.kingsrook.qbits.webhooks.model.WebhookEventCategory; -import com.kingsrook.qbits.webhooks.model.WebhooksActionFlags; -import com.kingsrook.qbits.webhooks.registry.WebhookEventType; -import com.kingsrook.qqq.backend.core.actions.customizers.OldRecordHelper; -import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.logging.QLogger; -import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; -import com.kingsrook.qqq.backend.core.utils.CollectionUtils; -import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; - - -/******************************************************************************* - ** post-update table customizer to fire webhook events - *******************************************************************************/ -public class FirePostUpdateWebhookEvent implements TableCustomizerInterface -{ - private static final QLogger LOG = QLogger.getLogger(FirePostUpdateWebhookEvent.class); - - - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public List postUpdate(UpdateInput updateInput, List records, Optional> oldRecordList) throws QException - { - if(updateInput.hasFlag(WebhooksActionFlags.OMIT_WEBHOOKS)) - { - LOG.debug("Requested to omit webhooks after update; returning early.", logPair("tableName", updateInput.getTableName())); - return (records); - } - - List updateWebhookEventTypes = WebhookSubscriptionsHelper.getWebhookEventTypesToConsiderFiringEventsFor(WebhookEventCategory.Kind.UPDATE, updateInput.getTableName()); - List storeWebhookEventTypes = WebhookSubscriptionsHelper.getWebhookEventTypesToConsiderFiringEventsFor(WebhookEventCategory.Kind.STORE, updateInput.getTableName()); - if(CollectionUtils.nullSafeIsEmpty(updateWebhookEventTypes) && CollectionUtils.nullSafeIsEmpty(storeWebhookEventTypes)) - { - return (records); - } - - List webhookEventTypes = new ArrayList<>(); - CollectionUtils.addAllIfNotNull(webhookEventTypes, updateWebhookEventTypes); - CollectionUtils.addAllIfNotNull(webhookEventTypes, storeWebhookEventTypes); - - OldRecordHelper oldRecordHelper = new OldRecordHelper(updateInput.getTableName(), oldRecordList); - - WebhookEventBuilder webhookEventBuilder = null; - for(WebhookEventType webhookEventType : webhookEventTypes) - { - QFieldMetaData field = FirePostInsertOrUpdateWebhookEventUtil.getQFieldMetaData(webhookEventType); - for(QRecord record : records) - { - if(CollectionUtils.nullSafeHasContents(record.getErrors())) - { - continue; - } - - if(record.getTableName() == null) - { - record.setTableName(updateInput.getTableName()); - } - - Optional oldRecord = oldRecordHelper.getOldRecord(record); - if(doesRecordMatchWebhookEventType(record, oldRecord, webhookEventType, field)) - { - ///////////////////////////////////////////////////////////////////////////////////////// - // in case the input record is "sparse" (e.g., missing some field values), and we have // - // the old record, then make a hybrid of the new + old for processing. // - ///////////////////////////////////////////////////////////////////////////////////////// - QRecord recordToProcess = record; - if(oldRecord.isPresent()) - { - recordToProcess = new QRecord(record); - for(Map.Entry entry : oldRecord.get().getValues().entrySet()) - { - if(!recordToProcess.getValues().containsKey(entry.getKey())) - { - recordToProcess.getValues().put(entry.getKey(), entry.getValue()); - } - } - } - - webhookEventBuilder = FirePostInsertOrUpdateWebhookEventUtil.processSubscriptions(updateInput.getTableName(), webhookEventType, recordToProcess, webhookEventBuilder, updateInput.getTransaction()); - } - } - } - - if(webhookEventBuilder != null) - { - webhookEventBuilder.storeWebhookEvents(updateInput.getTransaction()); - } - - return records; - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - boolean doesRecordMatchWebhookEventType(QRecord record, Optional optionalOldRecord, WebhookEventType webhookEventType, QFieldMetaData field) - { - switch(webhookEventType.getCategory()) - { - case UPDATE, STORE -> - { - return (true); - } - case UPDATE_WITH_FIELD, STORE_WITH_FIELD -> - { - if(optionalOldRecord.isEmpty()) - { - return (false); - } - - Serializable newValue = FirePostInsertOrUpdateWebhookEventUtil.getValue(record, field); - Serializable oldValue = FirePostInsertOrUpdateWebhookEventUtil.getValue(optionalOldRecord.get(), field); - if(!Objects.equals(newValue, oldValue)) - { - return (true); - } - } - case UPDATE_WITH_VALUE, STORE_WITH_VALUE -> - { - if(optionalOldRecord.isEmpty()) - { - return (false); - } - - Serializable newValue = FirePostInsertOrUpdateWebhookEventUtil.getValue(record, field); - Serializable oldValue = FirePostInsertOrUpdateWebhookEventUtil.getValue(optionalOldRecord.get(), field); - if(!Objects.equals(newValue, oldValue) && FirePostInsertOrUpdateWebhookEventUtil.doesNewValueMatchEventValue(webhookEventType, field, newValue)) - { - return (true); - } - } - default -> - { - return (false); - } - } - - return (false); - } - -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookEventBuilder.java b/src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookEventBuilder.java deleted file mode 100644 index 522ead3..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookEventBuilder.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.actions; - - -import java.util.ArrayList; -import java.util.List; -import com.kingsrook.qbits.webhooks.WebhooksQBitConfig; -import com.kingsrook.qbits.webhooks.model.Webhook; -import com.kingsrook.qbits.webhooks.model.WebhookEvent; -import com.kingsrook.qbits.webhooks.model.WebhookEventContent; -import com.kingsrook.qbits.webhooks.model.WebhookEventStatus; -import com.kingsrook.qbits.webhooks.model.WebhookSubscription; -import com.kingsrook.qbits.webhooks.registry.WebhookEventType; -import com.kingsrook.qbits.webhooks.registry.WebhooksRegistry; -import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; -import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; -import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; -import com.kingsrook.qqq.backend.core.context.QContext; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.logging.QLogger; -import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfig; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; -import com.kingsrook.qqq.backend.core.model.tables.QQQTableTableManager; -import com.kingsrook.qqq.backend.core.utils.CollectionUtils; -import com.kingsrook.qqq.backend.core.utils.ValueUtils; -import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; - - -/******************************************************************************* - ** class that build webhook events - *******************************************************************************/ -public class WebhookEventBuilder -{ - private static final QLogger LOG = QLogger.getLogger(WebhookEventBuilder.class); - - private List webhookEvents = null; - - - - /*************************************************************************** - ** - ***************************************************************************/ - public void addWebhookEvent(WebhookSubscription subscription, QRecord sourceRecord) - { - try - { - QTableMetaData table = QContext.getQInstance().getTable(sourceRecord.getTableName()); - Integer sourceRecordId = ValueUtils.getValueAsInteger(sourceRecord.getValue(table.getPrimaryKeyField())); - - Integer sourceTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), table.getName()); - - WebhookEvent webhookEvent = new WebhookEvent(); - webhookEvent.setWebhookId(subscription.getWebhookId()); - webhookEvent.setWebhookSubscriptionId(subscription.getId()); - webhookEvent.setWebhookEventTypeName(subscription.getWebhookEventTypeName()); - webhookEvent.setEventStatusId(WebhookEventStatus.NEW.getId()); - webhookEvent.setEventSourceRecordId(sourceRecordId); - webhookEvent.setEventSourceRecordQqqTableId(sourceTableId); - - webhookEvent.setContent(List.of(buildEventContent(sourceRecord, subscription.getWebhookEventTypeName(), subscription.getApiName(), subscription.getApiVersion()))); - - QRecord eventRecord = webhookEvent.toQRecord(); - - //////////////////////////////////////////////////////////////////////////// - // copy security field values from the sourceRecord into the event record // - //////////////////////////////////////////////////////////////////////////// - QTableMetaData webhookTable = QContext.getQInstance().getTable(Webhook.TABLE_NAME); - QBitConfig qbitConfig = webhookTable.getSourceQBitConfig(); - if(qbitConfig instanceof WebhooksQBitConfig webhooksQBitConfig) - { - for(QFieldMetaData field : CollectionUtils.nonNullList(webhooksQBitConfig.getSecurityFields())) - { - ////////////////////////////////////////////////////////////////////////////////////// - // todo - do we need to be more robust re: the field name within the source record? // - ////////////////////////////////////////////////////////////////////////////////////// - eventRecord.setValue(field.getName(), sourceRecord.getValue(field.getName())); - } - } - - if(webhookEvents == null) - { - webhookEvents = new ArrayList<>(); - } - - webhookEvents.add(eventRecord); - } - catch(Exception e) - { - LOG.warn("Error while creating webhook event", e, logPair("webhookSubscriptionId", () -> subscription.getId()), logPair("sourceRecord", sourceRecord)); - } - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - private static WebhookEventContent buildEventContent(QRecord sourceRecord, String webhookEventTypeName, String apiName, String apiVersion) throws QException - { - WebhookEventType webhookEventType = WebhooksRegistry.ofOrWithNew(QContext.getQInstance()).getWebhookEventType(webhookEventTypeName); - - WebhookEventTypeCustomizerInterface webhookEventTypeCustomizerInterface; - if(webhookEventType != null && webhookEventType.getCustomizer() != null) - { - webhookEventTypeCustomizerInterface = QCodeLoader.getAdHoc(WebhookEventTypeCustomizerInterface.class, webhookEventType.getCustomizer()); - } - else - { - webhookEventTypeCustomizerInterface = new DefaultWebhookEventTypeCustomizer(); - } - - return webhookEventTypeCustomizerInterface.buildEventContent(sourceRecord, webhookEventTypeName, apiName, apiVersion); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public void storeWebhookEvents(QBackendTransaction transaction) throws QException - { - if(webhookEvents != null) - { - new InsertAction().execute(new InsertInput(WebhookEvent.TABLE_NAME) - .withRecords(webhookEvents) - .withTransaction(transaction)); - } - } - -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookEventSender.java b/src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookEventSender.java deleted file mode 100644 index 1160942..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookEventSender.java +++ /dev/null @@ -1,417 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.actions; - - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.List; -import java.util.concurrent.TimeUnit; -import com.kingsrook.qbits.webhooks.WebhooksQBitConfig; -import com.kingsrook.qbits.webhooks.model.Webhook; -import com.kingsrook.qbits.webhooks.model.WebhookEvent; -import com.kingsrook.qbits.webhooks.model.WebhookEventContent; -import com.kingsrook.qbits.webhooks.model.WebhookEventSendLog; -import com.kingsrook.qbits.webhooks.model.WebhookEventStatus; -import com.kingsrook.qbits.webhooks.model.WebhookHealthStatus; -import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; -import com.kingsrook.qqq.backend.core.actions.audits.AuditAction; -import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; -import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; -import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.logging.QLogger; -import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; -import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.utils.CollectionUtils; -import com.kingsrook.qqq.backend.core.utils.SleepUtils; -import com.kingsrook.qqq.backend.core.utils.StringUtils; -import org.apache.http.HttpStatus; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.util.EntityUtils; -import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; - - -/******************************************************************************* - ** class that send webhook events to a webhook - *******************************************************************************/ -public class WebhookEventSender -{ - private static final QLogger LOG = QLogger.getLogger(WebhookEventSender.class); - - private static Integer maxAllowedAttempts = null; - private static Integer minutesToConsiderLeakedSendAttempt = null; - private static Integer[] backoffMinutes = null; - private static Integer maxAllowedRateLimitErrors = null; - private static Integer initialRateLimitBackoffMillis = null; - - - - /*************************************************************************** - ** - ***************************************************************************/ - public boolean handleEvent(WebhookEvent webhookEvent, Webhook webhook, List sendLogs) throws QException - { - ///////////////////// - // mark as sending // - ///////////////////// - Instant nextAttemptTimestamp = Instant.now().plus(getMinutesToConsiderLeakedSendAttempt() * 60, ChronoUnit.SECONDS); - updateWebhookEvent(webhookEvent.getId(), WebhookEventStatus.SENDING, nextAttemptTimestamp, null); - - ///////////////// - // try to send // - ///////////////// - WebhookEventSendLog sendLog = post(webhookEvent, webhook); - sendLog.setAttemptNo(sendLogs.size() + 1); - - InsertInput insertSendLogInput = new InsertInput(WebhookEventSendLog.TABLE_NAME).withRecordEntity(sendLog); - - try(QBackendTransaction transaction = QBackendTransaction.openFor(insertSendLogInput)) - { - insertSendLogInput.setTransaction(transaction); - if(sendLog.getSuccessful()) - { - /////////////////////// - // mark as delivered // - /////////////////////// - new InsertAction().execute(insertSendLogInput); - updateWebhookEvent(webhookEvent.getId(), WebhookEventStatus.DELIVERED, null, transaction); - transaction.commit(); - return (true); - } - else - { - ////////////////////////////////////////////////////////////////////////////////////////// - // consider if this event is now failed, or if it is retryable (based on # of failures) // - ////////////////////////////////////////////////////////////////////////////////////////// - WebhookEventStatus eventStatus = sendLog.getAttemptNo() >= getMaxAllowedAttempts() ? WebhookEventStatus.FAILED : WebhookEventStatus.AWAITING_RETRY; - if(eventStatus.equals(WebhookEventStatus.FAILED) && WebhookHealthStatus.PROBATION.getId().equals(webhook.getHealthStatusId())) - { - String message = "This Webhook Event has had too many failures, but since its Webhook's health status is Probation, its status will be Awaiting Retry instead of Failure."; - AuditAction.execute(WebhookEvent.TABLE_NAME, webhookEvent.getId(), null, message); - eventStatus = WebhookEventStatus.AWAITING_RETRY; - } - - /////////////////////////////////////////////////////////////////////// - // set the next-send time based on how many attempts there have been // - /////////////////////////////////////////////////////////////////////// - nextAttemptTimestamp = calculateNextAttemptBackoff(sendLog.getAttemptNo()); - if(eventStatus.equals(WebhookEventStatus.FAILED)) - { - nextAttemptTimestamp = null; - } - - new InsertAction().execute(insertSendLogInput); - updateWebhookEvent(webhookEvent.getId(), eventStatus, nextAttemptTimestamp, transaction); - transaction.commit(); - return (false); - } - } - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static Instant calculateNextAttemptBackoff(int attemptNumber) - { - Integer[] backoffMinutes = getBackoffMinutes(); - int index = Math.min(attemptNumber - 1, backoffMinutes.length - 1); - return Instant.now().plusSeconds(backoffMinutes[index] * 60); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public WebhookEventSendLog post(WebhookEvent webhookEvent, Webhook webhook) - { - WebhookEventSendLog sendLog = new WebhookEventSendLog(); - sendLog.setWebhookEventId(webhookEvent.getId()); - sendLog.setWebhookId(webhook.getId()); - sendLog.setStartTimestamp(Instant.now()); - - try - { - doPost(webhookEvent, webhook, sendLog); - - ////////////////////////////////////////////////// - // interpret no exception as meaning successful // - ////////////////////////////////////////////////// - sendLog.setSuccessful(true); - } - catch(WebhookPostException e) - { - /////////////////////////////////////////////////////////////////////////// - // any exception thrown means not successful - capture its error message // - /////////////////////////////////////////////////////////////////////////// - sendLog.setSuccessful(false); - sendLog.setErrorMessage(e.getMessage()); - } - - sendLog.setEndTimestamp(Instant.now()); - return (sendLog); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - protected void doPost(WebhookEvent webhookEvent, Webhook webhook, WebhookEventSendLog sendLog) throws WebhookPostException - { - int rateLimitsCaught = 0; - int rateLimitSleepMillis = getInitialRateLimitBackoffMillis(); - - /////////////////////////////////// - // loop in case of rate limiting // - /////////////////////////////////// - while(true) - { - try(CloseableHttpClient httpClient = buildHttpClient()) - { - HttpPost request = new HttpPost(webhook.getUrl()); - request.setHeader("Content-Type", "application/json"); - - String postBody = getPostBody(webhookEvent); - request.setEntity(new StringEntity(postBody, StandardCharsets.UTF_8)); - - try(CloseableHttpResponse response = executeHttpRequest(httpClient, request)) - { - int statusCode = response.getStatusLine().getStatusCode(); - String responseString = EntityUtils.toString(response.getEntity()); - - if(statusCode == HttpStatus.SC_TOO_MANY_REQUESTS) // 429 - { - throw (new WebhookRateLimitException(responseString)); - } - - sendLog.setHttpStatusCode(statusCode); - - if(statusCode < 200 || statusCode >= 300) - { - if(!StringUtils.hasContent(responseString)) - { - LOG.warn("Unsuccessful http status code, but no response body returned", logPair("statusCode", statusCode), logPair("webhookId", webhook.getId())); - responseString = "No response body returned"; - } - throw new WebhookPostException(responseString); - } - - //////////////////////////////////////////// - // successful exit of the rate-limit loop // - //////////////////////////////////////////// - break; - } - catch(WebhookRateLimitException rle) - { - rateLimitsCaught++; - if(rateLimitsCaught > getMaxAllowedRateLimitErrors()) - { - sendLog.setHttpStatusCode(HttpStatus.SC_TOO_MANY_REQUESTS); // 429 - LOG.warn("Giving up webhook post after too many rate limit errors", logPair("webhookId", webhook.getId()), logPair("rateLimitsCaught", rateLimitsCaught), logPair("response", rle.getMessage())); - throw (new WebhookPostException("Giving up after too many rate-limit errors (" + getMaxAllowedRateLimitErrors() + "). Latest response: " + rle.getMessage())); - } - - LOG.info("Caught rate limit. Will sleep and re-try", logPair("rateLimitsCaught", rateLimitsCaught), logPair("webhookId", webhook.getId()), logPair("sleeping", rateLimitSleepMillis)); - SleepUtils.sleep(rateLimitSleepMillis, TimeUnit.MILLISECONDS); - rateLimitSleepMillis *= 2; - } - catch(WebhookPostException wpe) - { - throw (wpe); - } - catch(Exception e) - { - LOG.warn("Exception executing http request", e, logPair("webhookId", webhook.getId())); - throw (new WebhookPostException(e.getMessage(), e)); - } - } - catch(WebhookPostException wpe) - { - throw (wpe); - } - catch(Exception e) - { - LOG.warn("Exception building http client", e, logPair("webhookId", webhook.getId())); - throw (new WebhookPostException(e.getMessage(), e)); - } - } - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - protected String getPostBody(WebhookEvent webhookEvent) throws QException - { - String postBody = null; - if(CollectionUtils.nullSafeIsEmpty(webhookEvent.getContent())) - { - List contentRecords = QueryAction.execute(WebhookEventContent.TABLE_NAME, new QQueryFilter(new QFilterCriteria("webhookEventId", QCriteriaOperator.EQUALS, webhookEvent.getId()))); - if(CollectionUtils.nullSafeHasContents(contentRecords)) - { - postBody = contentRecords.get(0).getValueString("postBody"); - } - } - else - { - postBody = webhookEvent.getContent().get(0).getPostBody(); - } - - if(!StringUtils.hasContent(postBody)) - { - throw (new QException("Missing content/postBody for webhook event: " + webhookEvent.getId())); - } - - return postBody; - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - protected CloseableHttpResponse executeHttpRequest(CloseableHttpClient httpClient, HttpPost request) throws IOException - { - return httpClient.execute(request); - } - - - - /******************************************************************************* - ** Build the default HttpClient used by the makeRequest method - *******************************************************************************/ - protected CloseableHttpClient buildHttpClient() - { - /////////////////////////////////////////////////////////////////////////////////////// - // do we want this?? .setConnectionManager(new PoolingHttpClientConnectionManager()) // - // needs some good scrutiny. // - /////////////////////////////////////////////////////////////////////////////////////// - return HttpClientBuilder.create() - .setDefaultRequestConfig(RequestConfig.custom() - .setConnectTimeout(5000) - .setConnectionRequestTimeout(5000) - .setSocketTimeout(5000).build()) - .build(); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - private void updateWebhookEvent(Integer id, WebhookEventStatus status, Instant nextAttemptTimestamp, QBackendTransaction transaction) throws QException - { - new UpdateAction().execute(new UpdateInput(WebhookEvent.TABLE_NAME) - .withTransaction(transaction) - .withRecord(new QRecord() - .withValue("id", id) - .withValue("nextAttemptTimestamp", nextAttemptTimestamp) - .withValue("eventStatusId", status.getId()))); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - protected int getMaxAllowedRateLimitErrors() - { - if(maxAllowedRateLimitErrors == null) - { - maxAllowedRateLimitErrors = WebhooksQBitConfig.getConfigValue(config -> config.getMaxRateLimitRetries()); - } - return (maxAllowedRateLimitErrors); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - protected int getInitialRateLimitBackoffMillis() - { - if(initialRateLimitBackoffMillis == null) - { - initialRateLimitBackoffMillis = WebhooksQBitConfig.getConfigValue(config -> config.getInitialRateLimitBackoffMillis()); - } - return (initialRateLimitBackoffMillis); - } - - - - /*************************************************************************** - * - ***************************************************************************/ - private static Integer getMaxAllowedAttempts() - { - if(maxAllowedAttempts == null) - { - maxAllowedAttempts = WebhooksQBitConfig.getConfigValue(config -> config.getMaxSendAttemptsBeforeFailure()); - } - return (maxAllowedAttempts); - } - - - - /*************************************************************************** - * - ***************************************************************************/ - private static Integer getMinutesToConsiderLeakedSendAttempt() - { - if(minutesToConsiderLeakedSendAttempt == null) - { - minutesToConsiderLeakedSendAttempt = WebhooksQBitConfig.getConfigValue(config -> config.getMinutesToConsiderLeakedSendAttempt()); - } - return (minutesToConsiderLeakedSendAttempt); - } - - - - /*************************************************************************** - * - ***************************************************************************/ - private static Integer[] getBackoffMinutes() - { - if(backoffMinutes == null) - { - List configValue = WebhooksQBitConfig.getConfigValue(config -> config.getMinutesBetweenRetryAttempts()); - backoffMinutes = configValue.toArray(new Integer[0]); - } - return (backoffMinutes); - } - -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookEventTypeCustomizerInterface.java b/src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookEventTypeCustomizerInterface.java deleted file mode 100644 index 54e4616..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookEventTypeCustomizerInterface.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.actions; - - -import java.io.Serializable; -import java.time.Instant; -import java.util.LinkedHashMap; -import java.util.Map; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.kingsrook.qbits.webhooks.model.WebhookEventContent; -import com.kingsrook.qbits.webhooks.registry.WebhookEventType; -import com.kingsrook.qbits.webhooks.registry.WebhooksRegistry; -import com.kingsrook.qqq.api.actions.QRecordApiAdapter; -import com.kingsrook.qqq.backend.core.context.QContext; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.utils.JsonUtils; -import com.kingsrook.qqq.backend.core.utils.StringUtils; - - -/******************************************************************************* - ** interface for doing work for a webhookEventType - where specific types can - ** customize behavior. - *******************************************************************************/ -public interface WebhookEventTypeCustomizerInterface -{ - - /*************************************************************************** - ** - ***************************************************************************/ - default WebhookEventContent buildEventContent(QRecord sourceRecord, String webhookEventTypeName, String apiName, String apiVersion) throws QException - { - WebhookEventType webhookEventType = WebhooksRegistry.of(QContext.getQInstance()).getWebhookEventType(webhookEventTypeName); - - Map postBody = new LinkedHashMap<>(); - - Map webhookEventDetails = new LinkedHashMap<>(); - postBody.put("webhookEventDetails", webhookEventDetails); - - webhookEventDetails.put("webhookEventTypeName", webhookEventTypeName); - webhookEventDetails.put("eventTimestamp", Instant.now().toString()); - - if(webhookEventType != null && StringUtils.hasContent(webhookEventType.getTableName())) - { - webhookEventDetails.put("tableName", webhookEventType.getTableName()); - } - - webhookEventDetails.put("apiName", apiName); - webhookEventDetails.put("apiVersion", apiVersion); - - Map apiRecord = QRecordApiAdapter.qRecordToApiMap(sourceRecord, sourceRecord.getTableName(), apiName, apiVersion); - postBody.put("record", apiRecord); - - customizeEventContent(sourceRecord, webhookEventTypeName, apiName, apiVersion, postBody); - - String json = JsonUtils.toJsonCustomized(postBody, builder -> - builder.serializationInclusion(JsonInclude.Include.ALWAYS)); - - return new WebhookEventContent().withPostBody(json); - } - - - /*************************************************************************** - ** - ***************************************************************************/ - default void customizeEventContent(QRecord sourceRecord, String webhookEventTypeName, String apiName, String apiVersion, Map postBody) throws QException - { - ///////////////////// - // noop by default // - ///////////////////// - } - -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookHealthManager.java b/src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookHealthManager.java deleted file mode 100644 index b18cea3..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookHealthManager.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.actions; - - -import java.util.Stack; -import com.kingsrook.qbits.webhooks.WebhooksQBitConfig; -import com.kingsrook.qbits.webhooks.model.Webhook; -import com.kingsrook.qbits.webhooks.model.WebhookEventSendLog; -import com.kingsrook.qbits.webhooks.model.WebhookHealthStatus; -import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.logging.QLogger; -import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.scheduler.processes.AbstractRecordSyncToScheduledJobProcess; -import org.apache.commons.lang3.BooleanUtils; -import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; - - -/******************************************************************************* - ** class that manages webhook health status - *******************************************************************************/ -public class WebhookHealthManager -{ - private static final QLogger LOG = QLogger.getLogger(WebhookHealthManager.class); - - - - /*************************************************************************** - ** - ***************************************************************************/ - public Webhook updateWebhookHealth(Stack recentSendLogs, Webhook webhook) throws QException - { - Integer repeatedFailsToGoUnhealthy = WebhooksQBitConfig.getConfigValue(config -> config.getRepeatedFailsToGoUnhealthy()); - boolean lastSuccessful = !recentSendLogs.isEmpty() && BooleanUtils.isTrue(recentSendLogs.peek().getSuccessful()); - if(lastSuccessful) - { - ////////////////////////////////////////////////////////////////// - // if the last send was successful, and the webhook had been on // - // probation, then take it off probation and mark it as healthy // - ////////////////////////////////////////////////////////////////// - if(WebhookHealthStatus.PROBATION.getId().equals(webhook.getHealthStatusId())) - { - LOG.info("Marking previously-probation webhook as healthy due to a success", logPair("id", webhook.getId())); - webhook = updateWebhookHealthStatus(webhook, WebhookHealthStatus.HEALTHY); - } - } - else - { - ///////////////////////////////////////// - // else the last send was a failure... // - ///////////////////////////////////////// - if(WebhookHealthStatus.PROBATION.getId().equals(webhook.getHealthStatusId())) - { - //////////////////////////////////////////////////////////////// - // if the webhook was on probation, put it back on probation. // - //////////////////////////////////////////////////////////////// - LOG.info("Marking previously-probation webhook as unhealthy due to a failure", logPair("id", webhook.getId())); - webhook = updateWebhookHealthStatus(webhook, WebhookHealthStatus.UNHEALTHY); - } - else if(WebhookHealthStatus.HEALTHY.getId().equals(webhook.getHealthStatusId())) - { - /////////////////////////////////////////////////////////////////////////////////////////////////// - // if the webhook was healthy - mark it as unhealthy if there have been enough failures in-a-row // - /////////////////////////////////////////////////////////////////////////////////////////////////// - if(repeatedFailsToGoUnhealthy != null && recentSendLogs.size() >= repeatedFailsToGoUnhealthy - && recentSendLogs.stream().noneMatch(sendLog -> BooleanUtils.isTrue(sendLog.getSuccessful()))) - { - LOG.info("Marking previously-healthy webhook as unhealthy due to repeated failures", logPair("id", webhook.getId()), logPair("repeatedFailLimit", repeatedFailsToGoUnhealthy)); - webhook = updateWebhookHealthStatus(webhook, WebhookHealthStatus.UNHEALTHY); - } - } - } - - return (webhook); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - private Webhook updateWebhookHealthStatus(Webhook webhook, WebhookHealthStatus status) throws QException - { - new UpdateAction().execute(new UpdateInput(Webhook.TABLE_NAME) - .withFlag(AbstractRecordSyncToScheduledJobProcess.ActionFlags.DO_NOT_SYNC) - .withRecord(new QRecord() - .withValue("id", webhook.getId()) - .withValue("healthStatusId", status.getId()))); - - webhook.setHealthStatusId(status.getId()); - return (webhook); - } - -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookPostException.java b/src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookPostException.java deleted file mode 100644 index a0b5f0e..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookPostException.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.actions; - - -import com.kingsrook.qqq.backend.core.exceptions.QException; - - -/******************************************************************************* - ** exception thrown while posting to a webhook - *******************************************************************************/ -public class WebhookPostException extends QException -{ - - /*************************************************************************** - ** - ***************************************************************************/ - public WebhookPostException(String message) - { - super(message); - } - - - /*************************************************************************** - ** - ***************************************************************************/ - public WebhookPostException(String message, Throwable cause) - { - super(message, cause); - } - -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookRateLimitException.java b/src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookRateLimitException.java deleted file mode 100644 index 8c965ff..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookRateLimitException.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.actions; - - -/******************************************************************************* - ** - *******************************************************************************/ -public class WebhookRateLimitException extends Exception -{ - /******************************************************************************* - ** Constructor - ** - *******************************************************************************/ - public WebhookRateLimitException(String message) - { - super(message); - } -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookSubscriptionsHelper.java b/src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookSubscriptionsHelper.java deleted file mode 100644 index 7b25353..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/actions/WebhookSubscriptionsHelper.java +++ /dev/null @@ -1,259 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.actions; - - -import java.io.Serializable; -import java.time.Duration; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import com.kingsrook.qbits.webhooks.WebhooksQBitConfig; -import com.kingsrook.qbits.webhooks.model.Webhook; -import com.kingsrook.qbits.webhooks.model.WebhookActiveStatus; -import com.kingsrook.qbits.webhooks.model.WebhookEventCategory; -import com.kingsrook.qbits.webhooks.model.WebhookSubscription; -import com.kingsrook.qbits.webhooks.registry.WebhookEventType; -import com.kingsrook.qbits.webhooks.registry.WebhooksRegistry; -import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; -import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; -import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper; -import com.kingsrook.qqq.backend.core.context.QContext; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfig; -import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType; -import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; -import com.kingsrook.qqq.backend.core.model.session.QSession; -import com.kingsrook.qqq.backend.core.utils.CollectionUtils; -import com.kingsrook.qqq.backend.core.utils.StringUtils; -import com.kingsrook.qqq.backend.core.utils.memoization.Memoization; - - -/******************************************************************************* - ** helper class for working with webhook subscriptions - *******************************************************************************/ -public class WebhookSubscriptionsHelper -{ - private static final Memoization> subscriptionsByEventTypeMemoization = new Memoization>() - .withTimeout(Duration.of(WebhooksQBitConfig.getConfigValue(config -> config.getMemoizationTimeoutMinutes()).longValue(), ChronoUnit.MINUTES)); - - private static final Memoization webhookBySubscriptionIdMemoization = new Memoization() - .withTimeout(Duration.of(WebhooksQBitConfig.getConfigValue(config -> config.getMemoizationTimeoutMinutes()).longValue(), ChronoUnit.MINUTES)); - - private static List allAccessSecurityKeysToUse = null; - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static void clearMemoizations() - { - subscriptionsByEventTypeMemoization.clear(); - webhookBySubscriptionIdMemoization.clear(); - } - - - /*************************************************************************** - ** - ***************************************************************************/ - public static List getWebhookEventTypesToConsiderFiringEventsFor(WebhookEventCategory.Kind kind, String tableName) - { - List eventTypes = null; - for(WebhookEventType webhookEventType : WebhooksRegistry.ofOrWithNew(QContext.getQInstance()).getAllWebhookEventTypes()) - { - if(webhookEventType.getCategory().getKind().equals(kind)) - { - if(!StringUtils.hasContent(webhookEventType.getTableName()) || Objects.equals(webhookEventType.getTableName(), tableName)) - { - if(CollectionUtils.nullSafeHasContents(getSubscriptionsForEventType(webhookEventType))) - { - eventTypes = Objects.requireNonNullElseGet(eventTypes, ArrayList::new); - eventTypes.add(webhookEventType); - } - } - } - } - - return (eventTypes); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static List getSubscriptionsForEventType(WebhookEventType webhookEventType) - { - return subscriptionsByEventTypeMemoization.getResultThrowing(webhookEventType.getName(), name -> - { - try - { - return QueryAction.execute(WebhookSubscription.TABLE_NAME, WebhookSubscription.class, new QQueryFilter() - .withCriteria(new QFilterCriteria("webhookEventTypeName", QCriteriaOperator.EQUALS, name)) - .withCriteria(new QFilterCriteria("activeStatusId", QCriteriaOperator.NOT_EQUALS, WebhookActiveStatus.DISABLED.getId())) - ); - } - catch(QException e) - { - throw new RuntimeException(e); - } - }).orElse(Collections.emptyList()); - } - - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static QRecord getWebhookForSubscription(WebhookSubscription webhookSubscription) - { - return webhookBySubscriptionIdMemoization.getResultThrowing(webhookSubscription.getId(), subscriptionId -> - { - try - { - List webhooks = new QueryAction().execute(new QueryInput(Webhook.TABLE_NAME) - .withFilter(new QQueryFilter(new QFilterCriteria(WebhookSubscription.TABLE_NAME + ".id", QCriteriaOperator.EQUALS, subscriptionId))) - .withQueryJoin(new QueryJoin(WebhookSubscription.TABLE_NAME))) - .getRecords(); - if(webhooks.isEmpty()) - { - return (null); - } - else - { - return (webhooks.get(0)); - } - } - catch(QException e) - { - throw new RuntimeException(e); - } - }).orElse(null); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static List getAllAccessSecurityKeysToUse() - { - if(allAccessSecurityKeysToUse == null) - { - List list = new ArrayList<>(); - - Map securityKeyTypeToAllAccessKeyNameMap = new HashMap<>(); - for(QSecurityKeyType securityKeyType : QContext.getQInstance().getSecurityKeyTypes().values()) - { - if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName())) - { - securityKeyTypeToAllAccessKeyNameMap.put(securityKeyType.getName(), securityKeyType.getAllAccessKeyName()); - } - } - - QTableMetaData webhookTable = QContext.getQInstance().getTable(Webhook.TABLE_NAME); - QBitConfig qbitConfig = webhookTable.getSourceQBitConfig(); - if(qbitConfig instanceof WebhooksQBitConfig webhooksQBitConfig) - { - for(RecordSecurityLock securityLock : CollectionUtils.nonNullList(webhooksQBitConfig.getRecordSecurityLocks())) - { - securityKeyTypeToAllAccessKeyNameMap.remove(securityLock.getSecurityKeyType()); - } - - list.addAll(securityKeyTypeToAllAccessKeyNameMap.values()); - } - - allAccessSecurityKeysToUse = list; - } - - return (allAccessSecurityKeysToUse); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static boolean doesRecordMatchSubscription(WebhookEventType webhookEventType, WebhookSubscription webhookSubscription, QRecord record, QBackendTransaction transaction) throws QException - { - String tableName = record.getTableName(); - QTableMetaData table = QContext.getQInstance().getTable(tableName); - - List recordSecurityLocks = table.getRecordSecurityLocks(); - if(CollectionUtils.nullSafeIsEmpty(recordSecurityLocks)) - { - //////////////////////////////////////////// - // if no locks on the table, always allow // - //////////////////////////////////////////// - return (true); - } - - QRecord webhookRecord = getWebhookForSubscription(webhookSubscription); - QTableMetaData webhookTable = QContext.getQInstance().getTable(Webhook.TABLE_NAME); - QBitConfig qbitConfig = webhookTable.getSourceQBitConfig(); - QSession pseudoSession = new QSession(); - - //////////////////////////////////////////////////////////////////////////////////////////////////// - // ask the qbit config what security locks are in place - e.g., values to copy from the webhook // - // record into a pseudo-session, to use for checking if the webhook is allowed to see the record. // - //////////////////////////////////////////////////////////////////////////////////////////////////// - if(qbitConfig instanceof WebhooksQBitConfig webhooksQBitConfig) - { - for(RecordSecurityLock securityLock : CollectionUtils.nonNullList(webhooksQBitConfig.getRecordSecurityLocks())) - { - Serializable securityFieldValue = webhookRecord.getValue(securityLock.getFieldName()); - pseudoSession.withSecurityKeyValue(securityLock.getSecurityKeyType(), securityFieldValue); - } - } - - ////////////////////////////////////////////////////////////// - // if there are no security locks on the qbit, always allow // - ////////////////////////////////////////////////////////////// - if(pseudoSession.getSecurityKeyValues().isEmpty()) - { - return (true); - } - - ////////////////////////////////////////////////////////////////////////////////////////////////// - // any keys in the instance that aren't used by the webhook qbit, if there's an all-access key, // - // then use it - to avoid records being blocked by a non-applicable lock // - ////////////////////////////////////////////////////////////////////////////////////////////////// - for(String allAccessKey : getAllAccessSecurityKeysToUse()) - { - pseudoSession.withSecurityKeyValue(allAccessKey, true); - } - - return (ValidateRecordSecurityLockHelper.allowedToReadRecord(table, record, pseudoSession, transaction)); - } -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/model/Webhook.java b/src/main/java/com/kingsrook/qbits/webhooks/model/Webhook.java deleted file mode 100644 index a6fb365..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/model/Webhook.java +++ /dev/null @@ -1,538 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.model; - - -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import com.kingsrook.qbits.webhooks.WebhooksQBitConfig; -import com.kingsrook.qbits.webhooks.actions.ClearWebhookSubscriptionHelperMemoizationsTableCustomizer; -import com.kingsrook.qbits.webhooks.processes.SyncWebhookScheduledJobProcess; -import com.kingsrook.qqq.backend.core.actions.customizers.MultiCustomizer; -import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; -import com.kingsrook.qqq.backend.core.model.data.QAssociation; -import com.kingsrook.qqq.backend.core.model.data.QField; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; -import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType; -import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; -import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; -import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface; -import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildJoin; -import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildRecordListWidget; -import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildTable; -import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity; -import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfig; -import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitProductionContext; -import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; -import com.kingsrook.qqq.backend.core.model.metadata.tables.Association; -import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory; -import com.kingsrook.qqq.backend.core.scheduler.processes.BaseSyncToScheduledJobTableCustomizer; -import com.kingsrook.qqq.backend.core.utils.CollectionUtils; -import com.kingsrook.qqq.backend.core.utils.collections.MutableList; - - -/******************************************************************************* - ** QRecord Entity for Webhook table - *******************************************************************************/ -@QMetaDataProducingEntity( - producePossibleValueSource = true, - produceTableMetaData = true, - tableMetaDataCustomizer = Webhook.TableMetaDataCustomizer.class, - childTables = { - @ChildTable( - childTableEntityClass = WebhookSubscription.class, - joinFieldName = "webhookId", - childJoin = @ChildJoin(enabled = true), - childRecordListWidget = @ChildRecordListWidget(label = "Subscriptions", enabled = true, maxRows = 250, canAddChildRecords = true, manageAssociationName = Webhook.SUBSCRIPTIONS_ASSOCIATION_NAME)), - @ChildTable( - childTableEntityClass = WebhookEvent.class, - joinFieldName = "webhookId", - childJoin = @ChildJoin(enabled = true), - childRecordListWidget = @ChildRecordListWidget(label = "Events", enabled = true, maxRows = 25, widgetMetaDataCustomizer = Webhook.EventAndEventSendLogChildRecordWidgetMetaDataCustomizer.class)), - @ChildTable( - childTableEntityClass = WebhookEventSendLog.class, - joinFieldName = "webhookId", - childJoin = @ChildJoin(enabled = true), - childRecordListWidget = @ChildRecordListWidget(label = "Event Send Log", enabled = true, maxRows = 25, widgetMetaDataCustomizer = Webhook.EventAndEventSendLogChildRecordWidgetMetaDataCustomizer.class)) - } -) -public class Webhook extends QRecordEntity -{ - public static final String TABLE_NAME = "webhook"; - public static final String SUBSCRIPTIONS_ASSOCIATION_NAME = "subscriptions"; - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static class TableMetaDataCustomizer implements MetaDataCustomizerInterface - { - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public QTableMetaData customizeMetaData(QInstance qInstance, QTableMetaData table) throws QException - { - String subscriptionChildJoinName = QJoinMetaData.makeInferredJoinName(Webhook.TABLE_NAME, WebhookSubscription.TABLE_NAME); - String eventChildJoinName = QJoinMetaData.makeInferredJoinName(Webhook.TABLE_NAME, WebhookEvent.TABLE_NAME); - String eventSendLogChildJoinName = QJoinMetaData.makeInferredJoinName(Webhook.TABLE_NAME, WebhookEventSendLog.TABLE_NAME); - - QFieldSection t1section = SectionFactory.defaultT1("id", "name", "description"); - - table - .withIcon(new QIcon().withName("webhook")) - .withRecordLabelFormat("%s") - .withRecordLabelFields("name") - .withSection(t1section) - .withSection(SectionFactory.defaultT2("url", "activeStatusId", "healthStatusId")) - .withSection(SectionFactory.customT2("subscriptions", new QIcon("subscriptions")).withWidgetName(subscriptionChildJoinName)) - .withSection(SectionFactory.customT2("events", new QIcon("notifications")).withWidgetName(eventChildJoinName)) - .withSection(SectionFactory.customT2("sendLog", new QIcon("receipt_long")).withWidgetName(eventSendLogChildJoinName)) - .withSection(SectionFactory.defaultT3("createDate", "modifyDate")) - .withExposedJoin(new ExposedJoin().withLabel("Subscriptions").withJoinPath(List.of(subscriptionChildJoinName)).withJoinTable(WebhookSubscription.TABLE_NAME)) - .withAssociation(new Association().withName(SUBSCRIPTIONS_ASSOCIATION_NAME).withAssociatedTableName(WebhookSubscription.TABLE_NAME).withJoinName(subscriptionChildJoinName)); - - addActiveStatusChipAdornment(table.getField("activeStatusId")); - addHealthStatusChipAdornment(table.getField("healthStatusId")); - - QBitConfig qBitConfig = QBitProductionContext.peekQBitConfig(); - if(qBitConfig instanceof WebhooksQBitConfig webhooksQBitConfig) - { - t1section.setFieldNames(new MutableList<>(t1section.getFieldNames())); - for(QFieldMetaData fieldMetaData : CollectionUtils.nonNullList(webhooksQBitConfig.getSecurityFields())) - { - table.addField(fieldMetaData.clone()); - t1section.getFieldNames().add(fieldMetaData.getName()); - } - - for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(webhooksQBitConfig.getRecordSecurityLocks())) - { - table.withRecordSecurityLock(recordSecurityLock); - } - } - - //////////////////////////////////////////////////////////////////////// - // we need 2 customizers to run post insert, update, and delete: // - // first, to clear the webhookSubscriptionHelper class's memoizations // - // second, to sync scheduled jobs for this table // - //////////////////////////////////////////////////////////////////////// - QCodeReference syncScheduledJobCustomizer = BaseSyncToScheduledJobTableCustomizer.makeCodeReference(table, new SyncWebhookScheduledJobProcess()); - QCodeReference postEverythingMultiCustomizer = MultiCustomizer.of(new QCodeReference(ClearWebhookSubscriptionHelperMemoizationsTableCustomizer.class), syncScheduledJobCustomizer); - table.withCustomizer(TableCustomizers.POST_INSERT_RECORD, postEverythingMultiCustomizer); - table.withCustomizer(TableCustomizers.POST_UPDATE_RECORD, postEverythingMultiCustomizer); - table.withCustomizer(TableCustomizers.POST_DELETE_RECORD, postEverythingMultiCustomizer); - - return (table); - } - - - - /*************************************************************************** - * - ***************************************************************************/ - public static QFieldMetaData addActiveStatusChipAdornment(QFieldMetaData field) - { - return field.withFieldAdornment(new FieldAdornment(AdornmentType.CHIP) - .withValues(AdornmentType.ChipValues.iconAndColorValues(WebhookActiveStatus.ACTIVE, "play_circle_outline", AdornmentType.ChipValues.COLOR_SUCCESS)) - .withValues(AdornmentType.ChipValues.iconAndColorValues(WebhookActiveStatus.PAUSED, "pause_circle_outline", AdornmentType.ChipValues.COLOR_WARNING)) - .withValues(AdornmentType.ChipValues.iconAndColorValues(WebhookActiveStatus.DISABLED, "not_interested", AdornmentType.ChipValues.COLOR_ERROR))); - - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static QFieldMetaData addHealthStatusChipAdornment(QFieldMetaData field) - { - return field.withFieldAdornment(new FieldAdornment(AdornmentType.CHIP) - .withValues(AdornmentType.ChipValues.iconAndColorValues(WebhookHealthStatus.HEALTHY, "favorite_border", AdornmentType.ChipValues.COLOR_SUCCESS)) - .withValues(AdornmentType.ChipValues.iconAndColorValues(WebhookHealthStatus.UNHEALTHY, "heart_broken", AdornmentType.ChipValues.COLOR_ERROR)) - .withValues(AdornmentType.ChipValues.iconAndColorValues(WebhookHealthStatus.PROBATION, "healing", AdornmentType.ChipValues.COLOR_WARNING))); - } - - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static class EventAndEventSendLogChildRecordWidgetMetaDataCustomizer implements MetaDataCustomizerInterface - { - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public QWidgetMetaData customizeMetaData(QInstance qInstance, QWidgetMetaData widget) throws QException - { - //////////////////////////// - // sort events by id desc // - //////////////////////////// - ArrayList orderBys = new ArrayList<>(List.of(new QFilterOrderBy("id", false))); - return widget.withDefaultValue("orderBy", orderBys); - } - } - - - - @QField(isEditable = false, isPrimaryKey = true) - private Integer id; - - @QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR, isRequired = true) - private String name; - - @QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR) - private String description; - - @QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR, isRequired = true, label = "URL") - private String url; - - @QField(isRequired = true, possibleValueSourceName = WebhookActiveStatus.NAME, defaultValue = WebhookActiveStatus.DEFAULT_VALUE) - private Integer activeStatusId; - - @QField(isRequired = true, possibleValueSourceName = WebhookHealthStatus.NAME, defaultValue = WebhookHealthStatus.DEFAULT_VALUE, isEditable = false) - private Integer healthStatusId; - - @QField(isEditable = false) - private Instant createDate; - - @QField(isEditable = false) - private Instant modifyDate; - - @QAssociation(name = SUBSCRIPTIONS_ASSOCIATION_NAME) - private List subscriptions; - - - - /******************************************************************************* - ** Default constructor - *******************************************************************************/ - public Webhook() - { - } - - - - /******************************************************************************* - ** Constructor that takes a QRecord - *******************************************************************************/ - public Webhook(QRecord record) - { - populateFromQRecord(record); - } - - - - /******************************************************************************* - ** Getter for id - *******************************************************************************/ - public Integer getId() - { - return (this.id); - } - - - - /******************************************************************************* - ** Setter for id - *******************************************************************************/ - public void setId(Integer id) - { - this.id = id; - } - - - - /******************************************************************************* - ** Fluent setter for id - *******************************************************************************/ - public Webhook withId(Integer id) - { - this.id = id; - return (this); - } - - - - /******************************************************************************* - ** Getter for name - *******************************************************************************/ - public String getName() - { - return (this.name); - } - - - - /******************************************************************************* - ** Setter for name - *******************************************************************************/ - public void setName(String name) - { - this.name = name; - } - - - - /******************************************************************************* - ** Fluent setter for name - *******************************************************************************/ - public Webhook withName(String name) - { - this.name = name; - return (this); - } - - - - /******************************************************************************* - ** Getter for description - *******************************************************************************/ - public String getDescription() - { - return (this.description); - } - - - - /******************************************************************************* - ** Setter for description - *******************************************************************************/ - public void setDescription(String description) - { - this.description = description; - } - - - - /******************************************************************************* - ** Fluent setter for description - *******************************************************************************/ - public Webhook withDescription(String description) - { - this.description = description; - return (this); - } - - - - /******************************************************************************* - ** Getter for url - *******************************************************************************/ - public String getUrl() - { - return (this.url); - } - - - - /******************************************************************************* - ** Setter for url - *******************************************************************************/ - public void setUrl(String url) - { - this.url = url; - } - - - - /******************************************************************************* - ** Fluent setter for url - *******************************************************************************/ - public Webhook withUrl(String url) - { - this.url = url; - return (this); - } - - - - /******************************************************************************* - ** Getter for createDate - *******************************************************************************/ - public Instant getCreateDate() - { - return (this.createDate); - } - - - - /******************************************************************************* - ** Setter for createDate - *******************************************************************************/ - public void setCreateDate(Instant createDate) - { - this.createDate = createDate; - } - - - - /******************************************************************************* - ** Fluent setter for createDate - *******************************************************************************/ - public Webhook withCreateDate(Instant createDate) - { - this.createDate = createDate; - return (this); - } - - - - /******************************************************************************* - ** Getter for modifyDate - *******************************************************************************/ - public Instant getModifyDate() - { - return (this.modifyDate); - } - - - - /******************************************************************************* - ** Setter for modifyDate - *******************************************************************************/ - public void setModifyDate(Instant modifyDate) - { - this.modifyDate = modifyDate; - } - - - - /******************************************************************************* - ** Fluent setter for modifyDate - *******************************************************************************/ - public Webhook withModifyDate(Instant modifyDate) - { - this.modifyDate = modifyDate; - return (this); - } - - - - /******************************************************************************* - ** Getter for subscriptions - *******************************************************************************/ - public List getSubscriptions() - { - return (this.subscriptions); - } - - - - /******************************************************************************* - ** Setter for subscriptions - *******************************************************************************/ - public void setSubscriptions(List subscriptions) - { - this.subscriptions = subscriptions; - } - - - - /******************************************************************************* - ** Fluent setter for subscriptions - *******************************************************************************/ - public Webhook withSubscriptions(List subscriptions) - { - this.subscriptions = subscriptions; - return (this); - } - - - - /******************************************************************************* - ** Getter for activeStatusId - *******************************************************************************/ - public Integer getActiveStatusId() - { - return (this.activeStatusId); - } - - - - /******************************************************************************* - ** Setter for activeStatusId - *******************************************************************************/ - public void setActiveStatusId(Integer activeStatusId) - { - this.activeStatusId = activeStatusId; - } - - - - /******************************************************************************* - ** Fluent setter for activeStatusId - *******************************************************************************/ - public Webhook withActiveStatusId(Integer activeStatusId) - { - this.activeStatusId = activeStatusId; - return (this); - } - - - - /******************************************************************************* - ** Getter for healthStatusId - *******************************************************************************/ - public Integer getHealthStatusId() - { - return (this.healthStatusId); - } - - - - /******************************************************************************* - ** Setter for healthStatusId - *******************************************************************************/ - public void setHealthStatusId(Integer healthStatusId) - { - this.healthStatusId = healthStatusId; - } - - - - /******************************************************************************* - ** Fluent setter for healthStatusId - *******************************************************************************/ - public Webhook withHealthStatusId(Integer healthStatusId) - { - this.healthStatusId = healthStatusId; - return (this); - } - -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookActiveStatus.java b/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookActiveStatus.java deleted file mode 100644 index ca2895b..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookActiveStatus.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.model; - - -import java.util.Objects; -import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum; -import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingPossibleValueEnum; - - -/******************************************************************************* - ** - *******************************************************************************/ -@QMetaDataProducingPossibleValueEnum() -public enum WebhookActiveStatus implements PossibleValueEnum -{ - ACTIVE(1, "Active"), - PAUSED(2, "Paused"), - DISABLED(3, "Disabled"); - - private final Integer id; - private final String label; - - public static final String NAME = "WebhookActiveStatus"; - - public static final String DEFAULT_VALUE = "1"; - - static - { - Objects.requireNonNull(getById(Integer.parseInt(DEFAULT_VALUE)), "Default value '" + DEFAULT_VALUE + " in WebhookActiveStatus is not a defined enum value"); - } - - /******************************************************************************* - ** - *******************************************************************************/ - WebhookActiveStatus(Integer id, String label) - { - this.id = id; - this.label = label; - } - - - - /******************************************************************************* - ** Get instance by id - ** - *******************************************************************************/ - public static WebhookActiveStatus getById(Integer id) - { - if(id == null) - { - return (null); - } - - for(WebhookActiveStatus value : WebhookActiveStatus.values()) - { - if(Objects.equals(value.id, id)) - { - return (value); - } - } - - return (null); - } - - - - /******************************************************************************* - ** Getter for id - ** - *******************************************************************************/ - public Integer getId() - { - return id; - } - - - - /******************************************************************************* - ** Getter for label - ** - *******************************************************************************/ - public String getLabel() - { - return label; - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Override - public Integer getPossibleValueId() - { - return (getId()); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Override - public String getPossibleValueLabel() - { - return (getLabel()); - } -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookEvent.java b/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookEvent.java deleted file mode 100644 index 027a565..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookEvent.java +++ /dev/null @@ -1,557 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.model; - - -import java.time.Instant; -import java.util.List; -import com.kingsrook.qbits.webhooks.WebhooksQBitConfig; -import com.kingsrook.qbits.webhooks.registry.WebhookEventTypePossibleValueSource; -import com.kingsrook.qbits.webhooks.tables.WebhookEventTableCustomizer; -import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.data.QAssociation; -import com.kingsrook.qqq.backend.core.model.data.QField; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; -import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType; -import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; -import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; -import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface; -import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildJoin; -import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildRecordListWidget; -import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildTable; -import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity; -import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfig; -import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitProductionContext; -import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; -import com.kingsrook.qqq.backend.core.model.metadata.tables.Association; -import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory; -import com.kingsrook.qqq.backend.core.model.tables.QQQTable; -import com.kingsrook.qqq.backend.core.utils.CollectionUtils; -import com.kingsrook.qqq.backend.core.utils.collections.MutableList; - - -/******************************************************************************* - ** QRecord Entity for WebhookEvent table - *******************************************************************************/ -@QMetaDataProducingEntity( - producePossibleValueSource = true, - produceTableMetaData = true, - tableMetaDataCustomizer = WebhookEvent.TableMetaDataCustomizer.class, - childTables = { - @ChildTable( - childTableEntityClass = WebhookEventSendLog.class, - joinFieldName = "webhookEventId", - childJoin = @ChildJoin(enabled = true), - childRecordListWidget = @ChildRecordListWidget(label = "Send Log", enabled = true)), - @ChildTable( - childTableEntityClass = WebhookEventContent.class, - joinFieldName = "webhookEventId", - childJoin = @ChildJoin(enabled = true, isOneToOne = true)) - } -) -public class WebhookEvent extends QRecordEntity -{ - public static final String TABLE_NAME = "webhookEvent"; - public static final String CONTENT_ASSOCIATION_NAME = "content"; - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static class TableMetaDataCustomizer implements MetaDataCustomizerInterface - { - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public QTableMetaData customizeMetaData(QInstance qInstance, QTableMetaData table) throws QException - { - String webhookParentJoinName = QJoinMetaData.makeInferredJoinName(Webhook.TABLE_NAME, WebhookEvent.TABLE_NAME); - String webhookSubscriptionParentJoinName = QJoinMetaData.makeInferredJoinName(WebhookSubscription.TABLE_NAME, WebhookEvent.TABLE_NAME); - String contentChildJoinName = QJoinMetaData.makeInferredJoinName(WebhookEvent.TABLE_NAME, WebhookEventContent.TABLE_NAME); - String sendLogChildJoinName = QJoinMetaData.makeInferredJoinName(WebhookEvent.TABLE_NAME, WebhookEventSendLog.TABLE_NAME); - - QFieldSection t1section = SectionFactory.defaultT1("id", "webhookId", "webhookSubscriptionId", "webhookEventTypeName"); - table - .withIcon(new QIcon().withName("notifications")) - .withRecordLabelFormat("%s") - .withRecordLabelFields("id") - .withSection(t1section) - .withSection(SectionFactory.defaultT2("eventStatusId", "eventSourceRecordQqqTableId", "eventSourceRecordId", "nextAttemptTimestamp")) - .withSection(SectionFactory.customT2("sendLogs", new QIcon("receipt_long")).withWidgetName(sendLogChildJoinName)) - .withSection(SectionFactory.customT2("content", new QIcon("description"), WebhookEventContent.TABLE_NAME + ".postBody")) - .withSection(SectionFactory.defaultT3("createDate", "modifyDate")) - .withExposedJoin(new ExposedJoin().withLabel("Webhook").withJoinPath(List.of(webhookParentJoinName)).withJoinTable(Webhook.TABLE_NAME)) - .withExposedJoin(new ExposedJoin().withLabel("Subscription").withJoinPath(List.of(webhookSubscriptionParentJoinName)).withJoinTable(WebhookSubscription.TABLE_NAME)) - .withExposedJoin(new ExposedJoin().withLabel("Send Log").withJoinPath(List.of(sendLogChildJoinName)).withJoinTable(WebhookEventSendLog.TABLE_NAME)) - .withExposedJoin(new ExposedJoin().withLabel("Content").withJoinPath(List.of(contentChildJoinName)).withJoinTable(WebhookEventContent.TABLE_NAME)) - .withAssociation(new Association().withName(CONTENT_ASSOCIATION_NAME).withJoinName(contentChildJoinName).withAssociatedTableName(WebhookEventContent.TABLE_NAME)); - - QBitConfig qBitConfig = QBitProductionContext.peekQBitConfig(); - if(qBitConfig instanceof WebhooksQBitConfig webhooksQBitConfig) - { - t1section.setFieldNames(new MutableList<>(t1section.getFieldNames())); - for(QFieldMetaData fieldMetaData : CollectionUtils.nonNullList(webhooksQBitConfig.getSecurityFields())) - { - table.addField(fieldMetaData.clone()); - t1section.getFieldNames().add(fieldMetaData.getName()); - } - - for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(webhooksQBitConfig.getRecordSecurityLocks())) - { - table.withRecordSecurityLock(recordSecurityLock); - } - } - - table.withCustomizer(TableCustomizers.POST_QUERY_RECORD, new QCodeReference(WebhookEventTableCustomizer.class)); - table.getField("eventSourceRecordId").withFieldAdornment(new FieldAdornment(AdornmentType.LINK).withValue(AdornmentType.LinkValues.TO_RECORD_FROM_TABLE_DYNAMIC, true)); - - QFieldMetaData eventStatusIdField = table.getField("eventStatusId"); - addEventStatusChipAdornment(eventStatusIdField); - - return (table); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static QFieldMetaData addEventStatusChipAdornment(QFieldMetaData eventStatusIdField) - { - eventStatusIdField.withFieldAdornment(new FieldAdornment(AdornmentType.CHIP) - .withValues(AdornmentType.ChipValues.iconAndColorValues(WebhookEventStatus.NEW, "pending", AdornmentType.ChipValues.COLOR_DEFAULT)) - .withValues(AdornmentType.ChipValues.iconAndColorValues(WebhookEventStatus.SENDING, "send", AdornmentType.ChipValues.COLOR_INFO)) - .withValues(AdornmentType.ChipValues.iconAndColorValues(WebhookEventStatus.DELIVERED, "check", AdornmentType.ChipValues.COLOR_SUCCESS)) - .withValues(AdornmentType.ChipValues.iconAndColorValues(WebhookEventStatus.AWAITING_RETRY, "hourglass_top", AdornmentType.ChipValues.COLOR_WARNING)) - .withValues(AdornmentType.ChipValues.iconAndColorValues(WebhookEventStatus.FAILED, "error", AdornmentType.ChipValues.COLOR_ERROR))); - return eventStatusIdField; - } - } - - - - @QField(isEditable = false, isPrimaryKey = true) - private Integer id; - - @QField(possibleValueSourceName = Webhook.TABLE_NAME) - private Integer webhookId; - - @QField(possibleValueSourceName = WebhookSubscription.TABLE_NAME) - private Integer webhookSubscriptionId; - - @QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR, isRequired = true, possibleValueSourceName = WebhookEventTypePossibleValueSource.NAME, label = "Event Type") - private String webhookEventTypeName; - - @QField(label = "Event Source Record Table", possibleValueSourceName = QQQTable.TABLE_NAME) - private Integer eventSourceRecordQqqTableId; - - @QField() - private Integer eventSourceRecordId; - - @QField(possibleValueSourceName = WebhookEventStatus.NAME) - private Integer eventStatusId; - - @QField(isEditable = false) - private Instant createDate; - - @QField(isEditable = false) - private Instant modifyDate; - - @QField(isEditable = false) - private Instant nextAttemptTimestamp; - - @QAssociation(name = CONTENT_ASSOCIATION_NAME) - private List content; - - - - /******************************************************************************* - ** Default constructor - *******************************************************************************/ - public WebhookEvent() - { - } - - - - /******************************************************************************* - ** Constructor that takes a QRecord - *******************************************************************************/ - public WebhookEvent(QRecord record) - { - populateFromQRecord(record); - } - - - - /******************************************************************************* - ** Getter for id - *******************************************************************************/ - public Integer getId() - { - return (this.id); - } - - - - /******************************************************************************* - ** Setter for id - *******************************************************************************/ - public void setId(Integer id) - { - this.id = id; - } - - - - /******************************************************************************* - ** Fluent setter for id - *******************************************************************************/ - public WebhookEvent withId(Integer id) - { - this.id = id; - return (this); - } - - - - /******************************************************************************* - ** Getter for webhookId - *******************************************************************************/ - public Integer getWebhookId() - { - return (this.webhookId); - } - - - - /******************************************************************************* - ** Setter for webhookId - *******************************************************************************/ - public void setWebhookId(Integer webhookId) - { - this.webhookId = webhookId; - } - - - - /******************************************************************************* - ** Fluent setter for webhookId - *******************************************************************************/ - public WebhookEvent withWebhookId(Integer webhookId) - { - this.webhookId = webhookId; - return (this); - } - - - - /******************************************************************************* - ** Getter for webhookSubscriptionId - *******************************************************************************/ - public Integer getWebhookSubscriptionId() - { - return (this.webhookSubscriptionId); - } - - - - /******************************************************************************* - ** Setter for webhookSubscriptionId - *******************************************************************************/ - public void setWebhookSubscriptionId(Integer webhookSubscriptionId) - { - this.webhookSubscriptionId = webhookSubscriptionId; - } - - - - /******************************************************************************* - ** Fluent setter for webhookSubscriptionId - *******************************************************************************/ - public WebhookEvent withWebhookSubscriptionId(Integer webhookSubscriptionId) - { - this.webhookSubscriptionId = webhookSubscriptionId; - return (this); - } - - - - /******************************************************************************* - ** Getter for webhookEventTypeName - *******************************************************************************/ - public String getWebhookEventTypeName() - { - return (this.webhookEventTypeName); - } - - - - /******************************************************************************* - ** Setter for webhookEventTypeName - *******************************************************************************/ - public void setWebhookEventTypeName(String webhookEventTypeName) - { - this.webhookEventTypeName = webhookEventTypeName; - } - - - - /******************************************************************************* - ** Fluent setter for webhookEventTypeName - *******************************************************************************/ - public WebhookEvent withWebhookEventTypeName(String webhookEventTypeName) - { - this.webhookEventTypeName = webhookEventTypeName; - return (this); - } - - - - /******************************************************************************* - ** Getter for eventSourceRecordQqqTableId - *******************************************************************************/ - public Integer getEventSourceRecordQqqTableId() - { - return (this.eventSourceRecordQqqTableId); - } - - - - /******************************************************************************* - ** Setter for eventSourceRecordQqqTableId - *******************************************************************************/ - public void setEventSourceRecordQqqTableId(Integer eventSourceRecordQqqTableId) - { - this.eventSourceRecordQqqTableId = eventSourceRecordQqqTableId; - } - - - - /******************************************************************************* - ** Fluent setter for eventSourceRecordQqqTableId - *******************************************************************************/ - public WebhookEvent withEventSourceRecordQqqTableId(Integer eventSourceRecordQqqTableId) - { - this.eventSourceRecordQqqTableId = eventSourceRecordQqqTableId; - return (this); - } - - - - /******************************************************************************* - ** Getter for eventSourceRecordId - *******************************************************************************/ - public Integer getEventSourceRecordId() - { - return (this.eventSourceRecordId); - } - - - - /******************************************************************************* - ** Setter for eventSourceRecordId - *******************************************************************************/ - public void setEventSourceRecordId(Integer eventSourceRecordId) - { - this.eventSourceRecordId = eventSourceRecordId; - } - - - - /******************************************************************************* - ** Fluent setter for eventSourceRecordId - *******************************************************************************/ - public WebhookEvent withEventSourceRecordId(Integer eventSourceRecordId) - { - this.eventSourceRecordId = eventSourceRecordId; - return (this); - } - - - - /******************************************************************************* - ** Getter for eventStatusId - *******************************************************************************/ - public Integer getEventStatusId() - { - return (this.eventStatusId); - } - - - - /******************************************************************************* - ** Setter for eventStatusId - *******************************************************************************/ - public void setEventStatusId(Integer eventStatusId) - { - this.eventStatusId = eventStatusId; - } - - - - /******************************************************************************* - ** Fluent setter for eventStatusId - *******************************************************************************/ - public WebhookEvent withEventStatusId(Integer eventStatusId) - { - this.eventStatusId = eventStatusId; - return (this); - } - - - - /******************************************************************************* - ** Getter for createDate - *******************************************************************************/ - public Instant getCreateDate() - { - return (this.createDate); - } - - - - /******************************************************************************* - ** Setter for createDate - *******************************************************************************/ - public void setCreateDate(Instant createDate) - { - this.createDate = createDate; - } - - - - /******************************************************************************* - ** Fluent setter for createDate - *******************************************************************************/ - public WebhookEvent withCreateDate(Instant createDate) - { - this.createDate = createDate; - return (this); - } - - - - /******************************************************************************* - ** Getter for modifyDate - *******************************************************************************/ - public Instant getModifyDate() - { - return (this.modifyDate); - } - - - - /******************************************************************************* - ** Setter for modifyDate - *******************************************************************************/ - public void setModifyDate(Instant modifyDate) - { - this.modifyDate = modifyDate; - } - - - - /******************************************************************************* - ** Fluent setter for modifyDate - *******************************************************************************/ - public WebhookEvent withModifyDate(Instant modifyDate) - { - this.modifyDate = modifyDate; - return (this); - } - - - /******************************************************************************* - ** Getter for content - *******************************************************************************/ - public List getContent() - { - return (this.content); - } - - - - /******************************************************************************* - ** Setter for content - *******************************************************************************/ - public void setContent(List content) - { - this.content = content; - } - - - - /******************************************************************************* - ** Fluent setter for content - *******************************************************************************/ - public WebhookEvent withContent(List content) - { - this.content = content; - return (this); - } - - - - /******************************************************************************* - ** Getter for nextAttemptTimestamp - *******************************************************************************/ - public Instant getNextAttemptTimestamp() - { - return (this.nextAttemptTimestamp); - } - - - - /******************************************************************************* - ** Setter for nextAttemptTimestamp - *******************************************************************************/ - public void setNextAttemptTimestamp(Instant nextAttemptTimestamp) - { - this.nextAttemptTimestamp = nextAttemptTimestamp; - } - - - - /******************************************************************************* - ** Fluent setter for nextAttemptTimestamp - *******************************************************************************/ - public WebhookEvent withNextAttemptTimestamp(Instant nextAttemptTimestamp) - { - this.nextAttemptTimestamp = nextAttemptTimestamp; - return (this); - } - - -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookEventCategory.java b/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookEventCategory.java deleted file mode 100644 index 9e1aeac..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookEventCategory.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.model; - - -import java.util.Objects; -import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum; -import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingPossibleValueEnum; - - -/******************************************************************************* - ** WebhookEventCategory - possible value enum - *******************************************************************************/ -@QMetaDataProducingPossibleValueEnum() -public enum WebhookEventCategory implements PossibleValueEnum -{ - INSERT("insert", "Record Inserted", Kind.INSERT, true, false, false), - INSERT_WITH_FIELD("insert-with-field", "Record Inserted with Field", Kind.INSERT, true, true, false), - INSERT_WITH_VALUE("insert-with-value", "Record Inserted with Specific Value", Kind.INSERT, true, true, true), - - UPDATE("update", "Record Updated", Kind.UPDATE, true, false, false), - UPDATE_WITH_FIELD("update-with-field", "Record Updated with Field", Kind.UPDATE, true, true, false), - UPDATE_WITH_VALUE("update-with-value", "Record Updated with Specific Value", Kind.UPDATE, true, true, true), - - STORE("store", "Record Stored", Kind.STORE, true, false, false), - STORE_WITH_FIELD("store-with-field", "Record Stored with Field", Kind.STORE, true, true, false), - STORE_WITH_VALUE("store-with-value", "Record Stored with Specific Value", Kind.STORE, true, true, true), - - AD_HOC("ad-hoc", "Ad Hoc", Kind.AD_HOC, false, false, false); - - private final String id; - private final String label; - private final Kind kind; - private final boolean requiresTable; - private final boolean requiresField; - private final boolean requiresValue; - - public static final String NAME = "WebhookEventCategory"; - - - - /*************************************************************************** - ** - ***************************************************************************/ - public enum Kind - { - INSERT, - UPDATE, - STORE, - AD_HOC - } - - - /******************************************************************************* - ** - *******************************************************************************/ - WebhookEventCategory(String id, String label, Kind kind, boolean requiresTable, boolean requiresField, boolean requiresValue) - { - this.id = id; - this.label = label; - this.kind = kind; - this.requiresTable = requiresTable; - this.requiresField = requiresField; - this.requiresValue = requiresValue; - } - - - - /******************************************************************************* - ** Get instance by id - ** - *******************************************************************************/ - public static WebhookEventCategory getById(String id) - { - if(id == null) - { - return (null); - } - - for(WebhookEventCategory value : WebhookEventCategory.values()) - { - if(Objects.equals(value.id, id)) - { - return (value); - } - } - - return (null); - } - - - - /******************************************************************************* - ** Getter for id - ** - *******************************************************************************/ - public String getId() - { - return id; - } - - - - /******************************************************************************* - ** Getter for label - ** - *******************************************************************************/ - public String getLabel() - { - return label; - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Override - public String getPossibleValueId() - { - return (getId()); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Override - public String getPossibleValueLabel() - { - return (getLabel()); - } - - - - /******************************************************************************* - ** Getter for requiresTable - ** - *******************************************************************************/ - public boolean getRequiresTable() - { - return requiresTable; - } - - - - /******************************************************************************* - ** Getter for requiresField - ** - *******************************************************************************/ - public boolean getRequiresField() - { - return requiresField; - } - - - - /******************************************************************************* - ** Getter for requiresValue - ** - *******************************************************************************/ - public boolean getRequiresValue() - { - return requiresValue; - } - - - - /******************************************************************************* - ** Getter for kind - ** - *******************************************************************************/ - public Kind getKind() - { - return kind; - } -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookEventContent.java b/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookEventContent.java deleted file mode 100644 index 7cde917..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookEventContent.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.model; - - -import java.util.List; -import com.kingsrook.qbits.webhooks.WebhooksQBitConfig; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.data.QField; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType; -import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment; -import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; -import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface; -import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity; -import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfig; -import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitProductionContext; -import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; -import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory; -import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; -import com.kingsrook.qqq.backend.core.utils.CollectionUtils; - - -/******************************************************************************* - ** QRecord Entity for WebhookEventContent table - *******************************************************************************/ -@QMetaDataProducingEntity( - producePossibleValueSource = true, - produceTableMetaData = true, - tableMetaDataCustomizer = WebhookEventContent.TableMetaDataCustomizer.class -) -public class WebhookEventContent extends QRecordEntity -{ - public static final String TABLE_NAME = "webhookEventContent"; - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static class TableMetaDataCustomizer implements MetaDataCustomizerInterface - { - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public QTableMetaData customizeMetaData(QInstance qInstance, QTableMetaData table) throws QException - { - String parentJoinName = QJoinMetaData.makeInferredJoinName(WebhookEvent.TABLE_NAME, WebhookEventContent.TABLE_NAME); - - table - .withIcon(new QIcon().withName("description")) - .withUniqueKey(new UniqueKey("webhookEventId")) - .withRecordLabelFormat("Event %s") - .withRecordLabelFields("webhookEventId") - .withSection(SectionFactory.defaultT1("id", "webhookEventId")) - .withSection(SectionFactory.defaultT2("postBody")) - .withExposedJoin(new ExposedJoin().withLabel("Event").withJoinPath(List.of(parentJoinName)).withJoinTable(WebhookEvent.TABLE_NAME)); - - table.getField("postBody").withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR) - .withValue(AdornmentType.CodeEditorValues.languageMode("json"))); - - QBitConfig qBitConfig = QBitProductionContext.peekQBitConfig(); - if(qBitConfig instanceof WebhooksQBitConfig webhooksQBitConfig) - { - for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(webhooksQBitConfig.getRecordSecurityLocks())) - { - RecordSecurityLock lockClone = recordSecurityLock.clone(); - lockClone.setFieldName(WebhookEvent.TABLE_NAME + "." + lockClone.getFieldName()); - lockClone.setJoinNameChain(List.of(parentJoinName)); - table.withRecordSecurityLock(lockClone); - } - } - - return (table); - } - } - - - - @QField(isEditable = false, isPrimaryKey = true) - private Integer id; - - @QField(possibleValueSourceName = WebhookEvent.TABLE_NAME) - private Integer webhookEventId; - - /////////////////////////// - // text, so no maxLength // - /////////////////////////// - @QField() - private String postBody; - - // todo? /////////////////////////// - // todo? // text, so no maxLength // - // todo? /////////////////////////// - // todo? @QField() - // todo? private String headers; - - - - /******************************************************************************* - ** Default constructor - *******************************************************************************/ - public WebhookEventContent() - { - } - - - - /******************************************************************************* - ** Constructor that takes a QRecord - *******************************************************************************/ - public WebhookEventContent(QRecord record) - { - populateFromQRecord(record); - } - - - - /******************************************************************************* - ** Getter for id - *******************************************************************************/ - public Integer getId() - { - return (this.id); - } - - - - /******************************************************************************* - ** Setter for id - *******************************************************************************/ - public void setId(Integer id) - { - this.id = id; - } - - - - /******************************************************************************* - ** Fluent setter for id - *******************************************************************************/ - public WebhookEventContent withId(Integer id) - { - this.id = id; - return (this); - } - - - - /******************************************************************************* - ** Getter for webhookEventId - *******************************************************************************/ - public Integer getWebhookEventId() - { - return (this.webhookEventId); - } - - - - /******************************************************************************* - ** Setter for webhookEventId - *******************************************************************************/ - public void setWebhookEventId(Integer webhookEventId) - { - this.webhookEventId = webhookEventId; - } - - - - /******************************************************************************* - ** Fluent setter for webhookEventId - *******************************************************************************/ - public WebhookEventContent withWebhookEventId(Integer webhookEventId) - { - this.webhookEventId = webhookEventId; - return (this); - } - - - - /******************************************************************************* - ** Getter for postBody - *******************************************************************************/ - public String getPostBody() - { - return (this.postBody); - } - - - - /******************************************************************************* - ** Setter for postBody - *******************************************************************************/ - public void setPostBody(String postBody) - { - this.postBody = postBody; - } - - - - /******************************************************************************* - ** Fluent setter for postBody - *******************************************************************************/ - public WebhookEventContent withPostBody(String postBody) - { - this.postBody = postBody; - return (this); - } - -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookEventSendLog.java b/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookEventSendLog.java deleted file mode 100644 index 0733016..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookEventSendLog.java +++ /dev/null @@ -1,431 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.model; - - -import java.time.Instant; -import java.util.List; -import com.kingsrook.qbits.webhooks.WebhooksQBitConfig; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.data.QField; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType; -import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment; -import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; -import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; -import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface; -import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity; -import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfig; -import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitProductionContext; -import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; -import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory; -import com.kingsrook.qqq.backend.core.utils.CollectionUtils; - - -/******************************************************************************* - ** QRecord Entity for WebhookEventSendLog table - *******************************************************************************/ -@QMetaDataProducingEntity( - producePossibleValueSource = true, - produceTableMetaData = true, - tableMetaDataCustomizer = WebhookEventSendLog.TableMetaDataCustomizer.class -) -public class WebhookEventSendLog extends QRecordEntity -{ - public static final String TABLE_NAME = "webhookEventSendLog"; - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static class TableMetaDataCustomizer implements MetaDataCustomizerInterface - { - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public QTableMetaData customizeMetaData(QInstance qInstance, QTableMetaData table) throws QException - { - String parentJoinName = QJoinMetaData.makeInferredJoinName(WebhookEvent.TABLE_NAME, WebhookEventSendLog.TABLE_NAME); - - table - .withIcon(new QIcon().withName("receipt_long")) - .withRecordLabelFormat("Event %s Attempt %s") - .withRecordLabelFields("webhookEventId", "attemptNo") - .withSection(SectionFactory.defaultT1("id", "webhookId", "webhookEventId", "attemptNo")) - .withSection(SectionFactory.defaultT2("successful", "httpStatusCode", "errorMessage")) - .withSection(SectionFactory.defaultT3("startTimestamp", "endTimestamp")) - .withExposedJoin(new ExposedJoin().withLabel("Event").withJoinPath(List.of(parentJoinName)).withJoinTable(WebhookEvent.TABLE_NAME)); - - table.getField("successful").withFieldAdornment(new FieldAdornment(AdornmentType.CHIP) - .withValues(AdornmentType.ChipValues.iconAndColorValues(true, "done", AdornmentType.ChipValues.COLOR_SUCCESS)) - .withValues(AdornmentType.ChipValues.iconAndColorValues(false, "error", AdornmentType.ChipValues.COLOR_ERROR))); - - QBitConfig qBitConfig = QBitProductionContext.peekQBitConfig(); - if(qBitConfig instanceof WebhooksQBitConfig webhooksQBitConfig) - { - for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(webhooksQBitConfig.getRecordSecurityLocks())) - { - RecordSecurityLock lockClone = recordSecurityLock.clone(); - lockClone.setFieldName(WebhookEvent.TABLE_NAME + "." + lockClone.getFieldName()); - lockClone.setJoinNameChain(List.of(parentJoinName)); - table.withRecordSecurityLock(lockClone); - } - } - - return (table); - } - } - - - @QField(isEditable = false, isPrimaryKey = true) - private Integer id; - - @QField(possibleValueSourceName = Webhook.TABLE_NAME) - private Integer webhookId; - - @QField(possibleValueSourceName = WebhookEvent.TABLE_NAME) - private Integer webhookEventId; - - @QField(label = "HTTP Status Code") - private Integer httpStatusCode; - - @QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS) - private String errorMessage; - - @QField() - private Boolean successful; - - @QField() - private Integer attemptNo; - - @QField(isEditable = false) - private Instant startTimestamp; - - @QField(isEditable = false) - private Instant endTimestamp; - - - - /******************************************************************************* - ** Default constructor - *******************************************************************************/ - public WebhookEventSendLog() - { - } - - - - /******************************************************************************* - ** Constructor that takes a QRecord - *******************************************************************************/ - public WebhookEventSendLog(QRecord record) - { - populateFromQRecord(record); - } - - - /******************************************************************************* - ** Getter for id - *******************************************************************************/ - public Integer getId() - { - return (this.id); - } - - - - /******************************************************************************* - ** Setter for id - *******************************************************************************/ - public void setId(Integer id) - { - this.id = id; - } - - - - /******************************************************************************* - ** Fluent setter for id - *******************************************************************************/ - public WebhookEventSendLog withId(Integer id) - { - this.id = id; - return (this); - } - - - - /******************************************************************************* - ** Getter for webhookEventId - *******************************************************************************/ - public Integer getWebhookEventId() - { - return (this.webhookEventId); - } - - - - /******************************************************************************* - ** Setter for webhookEventId - *******************************************************************************/ - public void setWebhookEventId(Integer webhookEventId) - { - this.webhookEventId = webhookEventId; - } - - - - /******************************************************************************* - ** Fluent setter for webhookEventId - *******************************************************************************/ - public WebhookEventSendLog withWebhookEventId(Integer webhookEventId) - { - this.webhookEventId = webhookEventId; - return (this); - } - - - - /******************************************************************************* - ** Getter for httpStatusCode - *******************************************************************************/ - public Integer getHttpStatusCode() - { - return (this.httpStatusCode); - } - - - - /******************************************************************************* - ** Setter for httpStatusCode - *******************************************************************************/ - public void setHttpStatusCode(Integer httpStatusCode) - { - this.httpStatusCode = httpStatusCode; - } - - - - /******************************************************************************* - ** Fluent setter for httpStatusCode - *******************************************************************************/ - public WebhookEventSendLog withHttpStatusCode(Integer httpStatusCode) - { - this.httpStatusCode = httpStatusCode; - return (this); - } - - - - /******************************************************************************* - ** Getter for successful - *******************************************************************************/ - public Boolean getSuccessful() - { - return (this.successful); - } - - - - /******************************************************************************* - ** Setter for successful - *******************************************************************************/ - public void setSuccessful(Boolean successful) - { - this.successful = successful; - } - - - - /******************************************************************************* - ** Fluent setter for successful - *******************************************************************************/ - public WebhookEventSendLog withSuccessful(Boolean successful) - { - this.successful = successful; - return (this); - } - - - - /******************************************************************************* - ** Getter for attemptNo - *******************************************************************************/ - public Integer getAttemptNo() - { - return (this.attemptNo); - } - - - - /******************************************************************************* - ** Setter for attemptNo - *******************************************************************************/ - public void setAttemptNo(Integer attemptNo) - { - this.attemptNo = attemptNo; - } - - - - /******************************************************************************* - ** Fluent setter for attemptNo - *******************************************************************************/ - public WebhookEventSendLog withAttemptNo(Integer attemptNo) - { - this.attemptNo = attemptNo; - return (this); - } - - - - /******************************************************************************* - ** Getter for startTimestamp - *******************************************************************************/ - public Instant getStartTimestamp() - { - return (this.startTimestamp); - } - - - - /******************************************************************************* - ** Setter for startTimestamp - *******************************************************************************/ - public void setStartTimestamp(Instant startTimestamp) - { - this.startTimestamp = startTimestamp; - } - - - - /******************************************************************************* - ** Fluent setter for startTimestamp - *******************************************************************************/ - public WebhookEventSendLog withStartTimestamp(Instant startTimestamp) - { - this.startTimestamp = startTimestamp; - return (this); - } - - - - /******************************************************************************* - ** Getter for endTimestamp - *******************************************************************************/ - public Instant getEndTimestamp() - { - return (this.endTimestamp); - } - - - - /******************************************************************************* - ** Setter for endTimestamp - *******************************************************************************/ - public void setEndTimestamp(Instant endTimestamp) - { - this.endTimestamp = endTimestamp; - } - - - - /******************************************************************************* - ** Fluent setter for endTimestamp - *******************************************************************************/ - public WebhookEventSendLog withEndTimestamp(Instant endTimestamp) - { - this.endTimestamp = endTimestamp; - return (this); - } - - - - /******************************************************************************* - ** Getter for errorMessage - *******************************************************************************/ - public String getErrorMessage() - { - return (this.errorMessage); - } - - - - /******************************************************************************* - ** Setter for errorMessage - *******************************************************************************/ - public void setErrorMessage(String errorMessage) - { - this.errorMessage = errorMessage; - } - - - - /******************************************************************************* - ** Fluent setter for errorMessage - *******************************************************************************/ - public WebhookEventSendLog withErrorMessage(String errorMessage) - { - this.errorMessage = errorMessage; - return (this); - } - - - - /******************************************************************************* - ** Getter for webhookId - *******************************************************************************/ - public Integer getWebhookId() - { - return (this.webhookId); - } - - - - /******************************************************************************* - ** Setter for webhookId - *******************************************************************************/ - public void setWebhookId(Integer webhookId) - { - this.webhookId = webhookId; - } - - - - /******************************************************************************* - ** Fluent setter for webhookId - *******************************************************************************/ - public WebhookEventSendLog withWebhookId(Integer webhookId) - { - this.webhookId = webhookId; - return (this); - } - - -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookEventStatus.java b/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookEventStatus.java deleted file mode 100644 index b3ef0a2..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookEventStatus.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.model; - - -import java.util.Objects; -import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum; -import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingPossibleValueEnum; - - -/******************************************************************************* - ** - *******************************************************************************/ -@QMetaDataProducingPossibleValueEnum() -public enum WebhookEventStatus implements PossibleValueEnum -{ - NEW(1, "New"), - SENDING(2, "Sending"), - DELIVERED(3, "Delivered"), - AWAITING_RETRY(4, "Awaiting Retry"), - FAILED(5, "Failed"); - - private final Integer id; - private final String label; - - public static final String NAME = "WebhookEventStatus"; - - public static final String DEFAULT_VALUE = "1"; - - static - { - Objects.requireNonNull(getById(Integer.parseInt(DEFAULT_VALUE)), "Default value '" + DEFAULT_VALUE + " in WebhookEventStatus is not a defined enum value"); - } - - /******************************************************************************* - ** - *******************************************************************************/ - WebhookEventStatus(Integer id, String label) - { - this.id = id; - this.label = label; - } - - - - /******************************************************************************* - ** Get instance by id - ** - *******************************************************************************/ - public static WebhookEventStatus getById(Integer id) - { - if(id == null) - { - return (null); - } - - for(WebhookEventStatus value : WebhookEventStatus.values()) - { - if(Objects.equals(value.id, id)) - { - return (value); - } - } - - return (null); - } - - - - /******************************************************************************* - ** Getter for id - ** - *******************************************************************************/ - public Integer getId() - { - return id; - } - - - - /******************************************************************************* - ** Getter for label - ** - *******************************************************************************/ - public String getLabel() - { - return label; - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Override - public Integer getPossibleValueId() - { - return (getId()); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Override - public String getPossibleValueLabel() - { - return (getLabel()); - } -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookHealthStatus.java b/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookHealthStatus.java deleted file mode 100644 index 88fe55c..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookHealthStatus.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.model; - - -import java.util.Objects; -import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum; -import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingPossibleValueEnum; - - -/******************************************************************************* - ** - *******************************************************************************/ -@QMetaDataProducingPossibleValueEnum() -public enum WebhookHealthStatus implements PossibleValueEnum -{ - HEALTHY(1, "Healthy"), - UNHEALTHY(2, "Unhealthy"), - PROBATION(3, "Probation"); - - private final Integer id; - private final String label; - - public static final String NAME = "WebhookHealthStatus"; - - public static final String DEFAULT_VALUE = "1"; - - static - { - Objects.requireNonNull(getById(Integer.parseInt(DEFAULT_VALUE)), "Default value '" + DEFAULT_VALUE + " in WebhookHealthStatus is not a defined enum value"); - } - - /******************************************************************************* - ** - *******************************************************************************/ - WebhookHealthStatus(Integer id, String label) - { - this.id = id; - this.label = label; - } - - - - /******************************************************************************* - ** Get instance by id - ** - *******************************************************************************/ - public static WebhookHealthStatus getById(Integer id) - { - if(id == null) - { - return (null); - } - - for(WebhookHealthStatus value : WebhookHealthStatus.values()) - { - if(Objects.equals(value.id, id)) - { - return (value); - } - } - - return (null); - } - - - - /******************************************************************************* - ** Getter for id - ** - *******************************************************************************/ - public Integer getId() - { - return id; - } - - - - /******************************************************************************* - ** Getter for label - ** - *******************************************************************************/ - public String getLabel() - { - return label; - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Override - public Integer getPossibleValueId() - { - return (getId()); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Override - public String getPossibleValueLabel() - { - return (getLabel()); - } -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookSubscription.java b/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookSubscription.java deleted file mode 100644 index 8fe8e3d..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/model/WebhookSubscription.java +++ /dev/null @@ -1,471 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.model; - - -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import com.kingsrook.qbits.webhooks.WebhooksQBitConfig; -import com.kingsrook.qbits.webhooks.actions.ClearWebhookSubscriptionHelperMemoizationsTableCustomizer; -import com.kingsrook.qbits.webhooks.registry.WebhookEventTypePossibleValueSource; -import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataProvider; -import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; -import com.kingsrook.qqq.backend.core.model.data.QField; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; -import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType; -import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment; -import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; -import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; -import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface; -import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildJoin; -import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildRecordListWidget; -import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildTable; -import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity; -import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfig; -import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitProductionContext; -import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; -import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory; -import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; -import com.kingsrook.qqq.backend.core.utils.CollectionUtils; -import com.kingsrook.qqq.backend.core.utils.StringUtils; - - -/******************************************************************************* - ** QRecord Entity for WebhookSubscription table - *******************************************************************************/ -@QMetaDataProducingEntity( - producePossibleValueSource = true, - produceTableMetaData = true, - tableMetaDataCustomizer = WebhookSubscription.TableMetaDataCustomizer.class, - childTables = { - @ChildTable( - childTableEntityClass = WebhookEvent.class, - joinFieldName = "webhookId", - childJoin = @ChildJoin(enabled = true), - childRecordListWidget = @ChildRecordListWidget(label = "Events", enabled = true, maxRows = 25, widgetMetaDataCustomizer = WebhookSubscription.EventChildRecordWidgetMetaDataCustomizer.class)) - } -) -public class WebhookSubscription extends QRecordEntity -{ - public static final String TABLE_NAME = "webhookSubscription"; - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static class TableMetaDataCustomizer implements MetaDataCustomizerInterface - { - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public QTableMetaData customizeMetaData(QInstance qInstance, QTableMetaData table) throws QException - { - String parentJoinName = QJoinMetaData.makeInferredJoinName(Webhook.TABLE_NAME, WebhookSubscription.TABLE_NAME); - String childJoinName = QJoinMetaData.makeInferredJoinName(WebhookSubscription.TABLE_NAME, WebhookEvent.TABLE_NAME); - - table - .withIcon(new QIcon().withName("subscriptions")) - .withRecordLabelFormat("%s - %s") - .withRecordLabelFields("webhookId", "webhookEventTypeName") - .withUniqueKey(new UniqueKey("webhookEventTypeName", "webhookId")) - .withSection(SectionFactory.defaultT1("id", "webhookId", "webhookEventTypeName")) - .withSection(SectionFactory.defaultT2("activeStatusId", "apiName", "apiVersion")) - // todo - if adding filters in future .withSection(SectionFactory.customT2("hidden", new QIcon().withName("hidden"), "queryFilterJson").withIsHidden(true)) - // todo - if adding filters in future .withSection(SectionFactory.customT2("filter", new QIcon("filter_alt")).withWidgetName(WebhookSubscriptionQueryFilterWidgetMetaDataProducer.NAME)) - .withSection(SectionFactory.customT2("events", new QIcon("notifications")).withWidgetName(childJoinName)) - .withSection(SectionFactory.defaultT3("createDate", "modifyDate")) - .withExposedJoin(new ExposedJoin().withLabel("Webhook").withJoinPath(List.of(parentJoinName)).withJoinTable(Webhook.TABLE_NAME)); - - QBitConfig qBitConfig = QBitProductionContext.peekQBitConfig(); - if(qBitConfig instanceof WebhooksQBitConfig webhooksQBitConfig) - { - if(StringUtils.hasContent(webhooksQBitConfig.getDefaultApiNameForNewSubscriptions())) - { - table.getField("apiName").withDefaultValue(webhooksQBitConfig.getDefaultApiNameForNewSubscriptions()); - } - - if(StringUtils.hasContent(webhooksQBitConfig.getDefaultApiVersionForNewSubscriptions())) - { - table.getField("apiVersion").withDefaultValue(webhooksQBitConfig.getDefaultApiVersionForNewSubscriptions()); - } - - for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(webhooksQBitConfig.getRecordSecurityLocks())) - { - RecordSecurityLock lockClone = recordSecurityLock.clone(); - lockClone.setFieldName(Webhook.TABLE_NAME + "." + lockClone.getFieldName()); - lockClone.setJoinNameChain(List.of(parentJoinName)); - table.withRecordSecurityLock(lockClone); - } - } - - table.getField("activeStatusId").withFieldAdornment(new FieldAdornment(AdornmentType.CHIP) - .withValues(AdornmentType.ChipValues.iconAndColorValues(WebhookActiveStatus.ACTIVE, "play_circle_outline", AdornmentType.ChipValues.COLOR_SUCCESS)) - .withValues(AdornmentType.ChipValues.iconAndColorValues(WebhookActiveStatus.PAUSED, "pause_circle_outline", AdornmentType.ChipValues.COLOR_WARNING)) - .withValues(AdornmentType.ChipValues.iconAndColorValues(WebhookActiveStatus.DISABLED, "not_interested", AdornmentType.ChipValues.COLOR_ERROR))); - - // todo - if adding filters in future table.getField("queryFilterJson").withBehavior(new FilterJsonFieldDisplayValueFormatter()); - - /////////////////////////////////////////////////////////////////////////////////////////// - // to clear the webhookSubscriptionHelper class's memoizations post insert/update/delete // - /////////////////////////////////////////////////////////////////////////////////////////// - QCodeReference clearWebhookSubscriptionHerlpMemoizations = new QCodeReference(ClearWebhookSubscriptionHelperMemoizationsTableCustomizer.class); - table.withCustomizer(TableCustomizers.POST_INSERT_RECORD, clearWebhookSubscriptionHerlpMemoizations); - table.withCustomizer(TableCustomizers.POST_UPDATE_RECORD, clearWebhookSubscriptionHerlpMemoizations); - table.withCustomizer(TableCustomizers.POST_DELETE_RECORD, clearWebhookSubscriptionHerlpMemoizations); - - return (table); - } - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static class EventChildRecordWidgetMetaDataCustomizer implements MetaDataCustomizerInterface - { - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public QWidgetMetaData customizeMetaData(QInstance qInstance, QWidgetMetaData widget) throws QException - { - //////////////////////////// - // sort events by id desc // - //////////////////////////// - ArrayList orderBys = new ArrayList<>(List.of(new QFilterOrderBy("id", false))); - return widget.withDefaultValue("orderBy", orderBys); - } - } - - - - @QField(isEditable = false, isPrimaryKey = true) - private Integer id; - - @QField(isRequired = true, possibleValueSourceName = Webhook.TABLE_NAME) - private Integer webhookId; - - @QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR, isRequired = true, possibleValueSourceName = WebhookEventTypePossibleValueSource.NAME, label = "Event Type") - private String webhookEventTypeName; - - ////////////////////////////// - // text, so omit max length // - ////////////////////////////// - // todo - if adding filters in future @QField(label = "Filter") - // todo - if adding filters in future private String queryFilterJson; - - @QField(isRequired = true, possibleValueSourceName = WebhookActiveStatus.NAME, defaultValue = WebhookActiveStatus.DEFAULT_VALUE) - private Integer activeStatusId; - - @QField(isRequired = true, maxLength = 50, valueTooLongBehavior = ValueTooLongBehavior.ERROR, label = "API Name", possibleValueSourceName = ApiInstanceMetaDataProvider.API_NAME_PVS_NAME) - private String apiName; - - @QField(isRequired = true, maxLength = 50, valueTooLongBehavior = ValueTooLongBehavior.ERROR, label = "API Version", possibleValueSourceName = ApiInstanceMetaDataProvider.API_VERSION_PVS_NAME) - private String apiVersion; - - @QField(isRequired = true, isEditable = false) - private Instant createDate; - - @QField(isEditable = false) - private Instant modifyDate; - - - - /******************************************************************************* - ** Default constructor - *******************************************************************************/ - public WebhookSubscription() - { - } - - - - /******************************************************************************* - ** Constructor that takes a QRecord - *******************************************************************************/ - public WebhookSubscription(QRecord record) - { - populateFromQRecord(record); - } - - - - /******************************************************************************* - ** Getter for id - *******************************************************************************/ - public Integer getId() - { - return (this.id); - } - - - - /******************************************************************************* - ** Setter for id - *******************************************************************************/ - public void setId(Integer id) - { - this.id = id; - } - - - - /******************************************************************************* - ** Fluent setter for id - *******************************************************************************/ - public WebhookSubscription withId(Integer id) - { - this.id = id; - return (this); - } - - - - /******************************************************************************* - ** Getter for webhookId - *******************************************************************************/ - public Integer getWebhookId() - { - return (this.webhookId); - } - - - - /******************************************************************************* - ** Setter for webhookId - *******************************************************************************/ - public void setWebhookId(Integer webhookId) - { - this.webhookId = webhookId; - } - - - - /******************************************************************************* - ** Fluent setter for webhookId - *******************************************************************************/ - public WebhookSubscription withWebhookId(Integer webhookId) - { - this.webhookId = webhookId; - return (this); - } - - - - /******************************************************************************* - ** Getter for webhookEventTypeName - *******************************************************************************/ - public String getWebhookEventTypeName() - { - return (this.webhookEventTypeName); - } - - - - /******************************************************************************* - ** Setter for webhookEventTypeName - *******************************************************************************/ - public void setWebhookEventTypeName(String webhookEventTypeName) - { - this.webhookEventTypeName = webhookEventTypeName; - } - - - - /******************************************************************************* - ** Fluent setter for webhookEventTypeName - *******************************************************************************/ - public WebhookSubscription withWebhookEventTypeName(String webhookEventTypeName) - { - this.webhookEventTypeName = webhookEventTypeName; - return (this); - } - - - - /******************************************************************************* - ** Getter for apiName - *******************************************************************************/ - public String getApiName() - { - return (this.apiName); - } - - - - /******************************************************************************* - ** Setter for apiName - *******************************************************************************/ - public void setApiName(String apiName) - { - this.apiName = apiName; - } - - - - /******************************************************************************* - ** Fluent setter for apiName - *******************************************************************************/ - public WebhookSubscription withApiName(String apiName) - { - this.apiName = apiName; - return (this); - } - - - - /******************************************************************************* - ** Getter for apiVersion - *******************************************************************************/ - public String getApiVersion() - { - return (this.apiVersion); - } - - - - /******************************************************************************* - ** Setter for apiVersion - *******************************************************************************/ - public void setApiVersion(String apiVersion) - { - this.apiVersion = apiVersion; - } - - - - /******************************************************************************* - ** Fluent setter for apiVersion - *******************************************************************************/ - public WebhookSubscription withApiVersion(String apiVersion) - { - this.apiVersion = apiVersion; - return (this); - } - - - - /******************************************************************************* - ** Getter for createDate - *******************************************************************************/ - public Instant getCreateDate() - { - return (this.createDate); - } - - - - /******************************************************************************* - ** Setter for createDate - *******************************************************************************/ - public void setCreateDate(Instant createDate) - { - this.createDate = createDate; - } - - - - /******************************************************************************* - ** Fluent setter for createDate - *******************************************************************************/ - public WebhookSubscription withCreateDate(Instant createDate) - { - this.createDate = createDate; - return (this); - } - - - - /******************************************************************************* - ** Getter for modifyDate - *******************************************************************************/ - public Instant getModifyDate() - { - return (this.modifyDate); - } - - - - /******************************************************************************* - ** Setter for modifyDate - *******************************************************************************/ - public void setModifyDate(Instant modifyDate) - { - this.modifyDate = modifyDate; - } - - - - /******************************************************************************* - ** Fluent setter for modifyDate - *******************************************************************************/ - public WebhookSubscription withModifyDate(Instant modifyDate) - { - this.modifyDate = modifyDate; - return (this); - } - - - - /******************************************************************************* - ** Getter for activeStatusId - *******************************************************************************/ - public Integer getActiveStatusId() - { - return (this.activeStatusId); - } - - - - /******************************************************************************* - ** Setter for activeStatusId - *******************************************************************************/ - public void setActiveStatusId(Integer activeStatusId) - { - this.activeStatusId = activeStatusId; - } - - - - /******************************************************************************* - ** Fluent setter for activeStatusId - *******************************************************************************/ - public WebhookSubscription withActiveStatusId(Integer activeStatusId) - { - this.activeStatusId = activeStatusId; - return (this); - } - -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/model/WebhooksActionFlags.java b/src/main/java/com/kingsrook/qbits/webhooks/model/WebhooksActionFlags.java deleted file mode 100644 index b1311a0..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/model/WebhooksActionFlags.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.model; - - -import com.kingsrook.qqq.backend.core.model.actions.tables.ActionFlag; - - -/******************************************************************************* - ** flags that can be put in an insert/update/delete event, to control webhook - ** events. - *******************************************************************************/ -public enum WebhooksActionFlags implements ActionFlag -{ - /*************************************************************************** - ** cause webhook events to not be fired for the action. - ***************************************************************************/ - OMIT_WEBHOOKS -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/processes/ManageWebhookHealthProcessMetaDataProducer.java b/src/main/java/com/kingsrook/qbits/webhooks/processes/ManageWebhookHealthProcessMetaDataProducer.java deleted file mode 100644 index 30ee0ad..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/processes/ManageWebhookHealthProcessMetaDataProducer.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.processes; - - -import java.util.List; -import com.kingsrook.qbits.webhooks.model.Webhook; -import com.kingsrook.qbits.webhooks.model.WebhookHealthStatus; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducer; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; -import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; -import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep; -import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaUpdateStep; -import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess; - - -/******************************************************************************* - ** Meta Data Producer for ManageWebhookHealth - *******************************************************************************/ -public class ManageWebhookHealthProcessMetaDataProducer extends MetaDataProducer -{ - public static final String NAME = "ManageWebhookHealth"; - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Override - public QProcessMetaData produce(QInstance qInstance) throws QException - { - QProcessMetaData processMetaData = StreamedETLWithFrontendProcess.processMetaDataBuilder() - .withName(NAME) - .withTableName(Webhook.TABLE_NAME) - .withIcon(new QIcon().withName("healing")) - .withExtractStepClass(ExtractViaQueryStep.class) - .withTransformStepClass(ManageWebhookHealthTransformStep.class) - .withLoadStepClass(LoadViaUpdateStep.class) - .withSourceTable(Webhook.TABLE_NAME) - .withDestinationTable(Webhook.TABLE_NAME) - .withPreviewMessage(StreamedETLWithFrontendProcess.DEFAULT_PREVIEW_MESSAGE_FOR_UPDATE) - .withReviewStepRecordFields(List.of( - new QFieldMetaData("id", QFieldType.INTEGER), - new QFieldMetaData("name", QFieldType.STRING), - Webhook.TableMetaDataCustomizer.addHealthStatusChipAdornment(new QFieldMetaData("healthStatusId", QFieldType.INTEGER).withPossibleValueSourceName(WebhookHealthStatus.NAME)) - )) - .getProcessMetaData(); - - return (processMetaData); - } - -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/processes/ManageWebhookHealthTransformStep.java b/src/main/java/com/kingsrook/qbits/webhooks/processes/ManageWebhookHealthTransformStep.java deleted file mode 100644 index d5e047f..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/processes/ManageWebhookHealthTransformStep.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.processes; - - -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import com.kingsrook.qbits.webhooks.WebhooksQBitConfig; -import com.kingsrook.qbits.webhooks.model.Webhook; -import com.kingsrook.qbits.webhooks.model.WebhookEventSendLog; -import com.kingsrook.qbits.webhooks.model.WebhookHealthStatus; -import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.logging.QLogger; -import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine; -import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface; -import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; -import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; -import com.kingsrook.qqq.backend.core.model.actions.processes.Status; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep; -import com.kingsrook.qqq.backend.core.utils.CollectionUtils; - - -/******************************************************************************* - ** check if any unhealthy webhooks are ready to go into Probation - *******************************************************************************/ -public class ManageWebhookHealthTransformStep extends AbstractTransformStep -{ - private static final QLogger LOG = QLogger.getLogger(ManageWebhookHealthTransformStep.class); - - private ProcessSummaryLine willPutOnProbationLine = new ProcessSummaryLine(Status.OK) - .withMessageSuffix(" updated from Unhealthy to Probation") - .withSingularFutureMessage("will be") - .withPluralFutureMessage("will be") - .withSingularPastMessage("was") - .withPluralPastMessage("were"); - - private ProcessSummaryLine notUnhealthyLine = new ProcessSummaryLine(Status.INFO) - .withMessageSuffix(" not currently Unhealthy") - .withSingularFutureMessage("is") - .withPluralFutureMessage("are") - .withSingularPastMessage("was") - .withPluralPastMessage("were"); - - private ProcessSummaryLine notUnhealthyLongEnoughLine = new ProcessSummaryLine(Status.ERROR) - .withMessageSuffix(" Unhealthy long enough to be put on Probation") - .withSingularFutureMessage("has not been") - .withPluralFutureMessage("have not been") - .withSingularPastMessage("was not") - .withPluralPastMessage("were not"); - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Override - public ArrayList getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen) - { - ArrayList rs = new ArrayList<>(); - willPutOnProbationLine.addSelfToListIfAnyCount(rs); - notUnhealthyLine.addSelfToListIfAnyCount(rs); - notUnhealthyLongEnoughLine.addSelfToListIfAnyCount(rs); - return (rs); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Override - public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException - { - Set unhealthy = new HashSet<>(); - for(Webhook webhook : runBackendStepInput.getRecordsAsEntities(Webhook.class)) - { - if(WebhookHealthStatus.UNHEALTHY.getId().equals(webhook.getHealthStatusId())) - { - unhealthy.add(webhook); - } - else - { - notUnhealthyLine.incrementCountAndAddPrimaryKey(webhook.getId()); - } - } - - if(CollectionUtils.nullSafeIsEmpty(unhealthy)) - { - return; - } - - Integer timeoutMinutes = WebhooksQBitConfig.getConfigValue(config -> config.getUnhealthyToProbationTimeoutMinutes()); - Instant limitTime = Instant.now().minus(timeoutMinutes, ChronoUnit.MINUTES); - - for(Webhook webhook : unhealthy) - { - List eventLogs = new QueryAction().execute(new QueryInput(WebhookEventSendLog.TABLE_NAME).withFilter(new QQueryFilter() - .withCriteria(new QFilterCriteria("webhookId", QCriteriaOperator.EQUALS, webhook.getId())) - .withOrderBy(new QFilterOrderBy("id", false)) - .withLimit(1))).getRecordEntities(WebhookEventSendLog.class); - Instant lastEventTimestamp = Instant.now(); - if(!eventLogs.isEmpty() && eventLogs.get(0).getStartTimestamp() != null) - { - lastEventTimestamp = eventLogs.get(0).getStartTimestamp(); - } - - if(lastEventTimestamp.isBefore(limitTime)) - { - willPutOnProbationLine.incrementCountAndAddPrimaryKey(webhook.getId()); - runBackendStepOutput.addRecord(new QRecord() - .withValue("id", webhook.getId()) - .withValue("name", webhook.getName()) // just for preview screen - .withValue("healthStatusId", WebhookHealthStatus.PROBATION.getId())); - } - else - { - notUnhealthyLongEnoughLine.incrementCountAndAddPrimaryKey(webhook.getId()); - } - } - } - -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/processes/SendTestEventToWebhookLoadStep.java b/src/main/java/com/kingsrook/qbits/webhooks/processes/SendTestEventToWebhookLoadStep.java deleted file mode 100644 index 2439617..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/processes/SendTestEventToWebhookLoadStep.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.processes; - - -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import com.kingsrook.qbits.webhooks.actions.WebhookEventSender; -import com.kingsrook.qbits.webhooks.model.Webhook; -import com.kingsrook.qbits.webhooks.model.WebhookEvent; -import com.kingsrook.qbits.webhooks.model.WebhookEventContent; -import com.kingsrook.qbits.webhooks.model.WebhookEventSendLog; -import com.kingsrook.qbits.webhooks.model.WebhookHealthStatus; -import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.logging.QLogger; -import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine; -import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface; -import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; -import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; -import com.kingsrook.qqq.backend.core.model.actions.processes.Status; -import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractLoadStep; -import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ProcessSummaryProviderInterface; -import com.kingsrook.qqq.backend.core.processes.implementations.general.ProcessSummaryWarningsAndErrorsRollup; -import org.apache.commons.lang3.BooleanUtils; - - -/******************************************************************************* - ** sends test events to webhooks. - *******************************************************************************/ -public class SendTestEventToWebhookLoadStep extends AbstractLoadStep implements ProcessSummaryProviderInterface -{ - private static final QLogger LOG = QLogger.getLogger(SendTestEventToWebhookLoadStep.class); - - private ProcessSummaryLine okLine = new ProcessSummaryLine(Status.OK) - .withMessageSuffix(" delivered successfully") - .withSingularPastMessage("was") - .withPluralPastMessage("were"); - - private ProcessSummaryLine updatedToProbationLine = new ProcessSummaryLine(Status.INFO) - .withMessageSuffix(" updated from Unhealthy to Probation") - .withSingularPastMessage("was") - .withPluralPastMessage("were"); - - private ProcessSummaryWarningsAndErrorsRollup errorsRollup = new ProcessSummaryWarningsAndErrorsRollup() - .withDoReplaceSingletonCountLinesWithSuffixOnly(false) - .withErrorTemplate(new ProcessSummaryLine(Status.ERROR) - .withSingularPastMessage("failed to send with error message: ") - .withPluralPastMessage("failed to send with error message: ")) - .withOtherErrorsSummary(new ProcessSummaryLine(Status.ERROR) - .withSingularPastMessage("had an other error.") - .withPluralPastMessage("had other errors.")); - - - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public ArrayList getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen) - { - ArrayList rs = new ArrayList<>(); - okLine.addSelfToListIfAnyCount(rs); - updatedToProbationLine.addSelfToListIfAnyCount(rs); - errorsRollup.addToList(rs); - return rs; - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Override - public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException - { - boolean uponSuccessUpdateUnhealthyWebhooksToProbation = BooleanUtils.isTrue(runBackendStepInput.getValueBoolean("uponSuccessUpdateUnhealthyWebhooksToProbation")); - List webhooksToUpdate = new ArrayList<>(); - - for(Webhook webhook : runBackendStepInput.getRecordsAsEntities(Webhook.class)) - { - WebhookEventSendLog sendLog; - try - { - WebhookEvent event = new WebhookEvent().withContent(List.of(new WebhookEventContent() - .withPostBody(""" - { - "webhookEventDetails": - { - "webhookEventTypeName": "testEvent", - "eventTimestamp": "%s", - "tableName": null, - "apiName": null, - "apiVersion": null - }, - "record": - { - } - }""".formatted(Instant.now().toString())) - )); - - sendLog = getWebhookEventSender().post(event, webhook); - } - catch(Exception e) - { - sendLog = new WebhookEventSendLog() - .withSuccessful(false) - .withErrorMessage(e.getMessage()); - } - - if(sendLog.getSuccessful()) - { - if(WebhookHealthStatus.UNHEALTHY.getId().equals(webhook.getHealthStatusId())) - { - webhooksToUpdate.add(new QRecord().withValue("id", webhook.getId()).withValue("healthStatusId", WebhookHealthStatus.PROBATION.getId())); - updatedToProbationLine.incrementCountAndAddPrimaryKey(webhook.getId()); - } - - okLine.incrementCountAndAddPrimaryKey(webhook.getId()); - } - else - { - if(sendLog.getHttpStatusCode() != null) - { - errorsRollup.addError("Status Code: " + sendLog.getHttpStatusCode() + "; " + sendLog.getErrorMessage(), webhook.getId()); - } - else - { - errorsRollup.addError(sendLog.getErrorMessage(), webhook.getId()); - } - } - } - - if(uponSuccessUpdateUnhealthyWebhooksToProbation && !webhooksToUpdate.isEmpty()) - { - new UpdateAction().execute(new UpdateInput(Webhook.TABLE_NAME).withRecords(webhooksToUpdate)); - } - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - protected WebhookEventSender getWebhookEventSender() - { - return new WebhookEventSender(); - } -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/processes/SendTestEventToWebhookProcessMetaDataProducer.java b/src/main/java/com/kingsrook/qbits/webhooks/processes/SendTestEventToWebhookProcessMetaDataProducer.java deleted file mode 100644 index b589441..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/processes/SendTestEventToWebhookProcessMetaDataProducer.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.processes; - - -import java.util.List; -import com.kingsrook.qbits.webhooks.model.Webhook; -import com.kingsrook.qbits.webhooks.model.WebhookHealthStatus; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducer; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; -import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; -import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep; -import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.NoopTransformStep; -import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess; - - -/******************************************************************************* - ** Meta Data Producer for SendTestEventToWebhook - *******************************************************************************/ -public class SendTestEventToWebhookProcessMetaDataProducer extends MetaDataProducer -{ - public static final String NAME = "SendTestEventToWebhook"; - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Override - public QProcessMetaData produce(QInstance qInstance) throws QException - { - QProcessMetaData processMetaData = StreamedETLWithFrontendProcess.processMetaDataBuilder() - .withName(NAME) - .withTableName(Webhook.TABLE_NAME) - .withIcon(new QIcon().withName("quiz")) - .withExtractStepClass(ExtractViaQueryStep.class) - .withTransformStepClass(NoopTransformStep.class) - .withLoadStepClass(SendTestEventToWebhookLoadStep.class) - .withSourceTable(Webhook.TABLE_NAME) - .withDestinationTable(Webhook.TABLE_NAME) - .withPreviewMessage(StreamedETLWithFrontendProcess.DEFAULT_PREVIEW_MESSAGE_PREFIX + " sent a test event") - .withReviewStepRecordFields(List.of( - new QFieldMetaData("id", QFieldType.INTEGER), - new QFieldMetaData("name", QFieldType.STRING), - new QFieldMetaData("url", QFieldType.STRING), - Webhook.TableMetaDataCustomizer.addHealthStatusChipAdornment(new QFieldMetaData("healthStatusId", QFieldType.INTEGER).withPossibleValueSourceName(WebhookHealthStatus.NAME)) - )) - .getProcessMetaData(); - - processMetaData.withStep(0, new QFrontendStepMetaData() - .withName("input") - .withComponent(new QFrontendComponentMetaData() - .withType(QComponentType.EDIT_FORM)) - .withFormField(new QFieldMetaData("uponSuccessUpdateUnhealthyWebhooksToProbation", QFieldType.BOOLEAN) - .withDefaultValue("true") - .withLabel("Upon Success Update Unhealthy Webhooks To Probation?"))); - - return (processMetaData); - } - -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventExtractStep.java b/src/main/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventExtractStep.java deleted file mode 100644 index c24d63a..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventExtractStep.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.processes; - - -import java.time.Instant; -import com.kingsrook.qbits.webhooks.model.Webhook; -import com.kingsrook.qbits.webhooks.model.WebhookActiveStatus; -import com.kingsrook.qbits.webhooks.model.WebhookEventStatus; -import com.kingsrook.qbits.webhooks.model.WebhookHealthStatus; -import com.kingsrook.qqq.backend.core.actions.tables.GetAction; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.logging.QLogger; -import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep; -import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; - - -/******************************************************************************* - ** - *******************************************************************************/ -public class SendWebhookEventExtractStep extends ExtractViaQueryStep -{ - private static final QLogger LOG = QLogger.getLogger(SendWebhookEventExtractStep.class); - - private static QQueryFilter falseFilter = new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.FALSE)); - - - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - protected QQueryFilter getQueryFilter(RunBackendStepInput runBackendStepInput) throws QException - { - ///////////////////////////////////////////////////////////////////////////////////////////////////////// - // if we've been asked to run for a specific webhook, then look up events that need sent for that hook // - ///////////////////////////////////////////////////////////////////////////////////////////////////////// - Integer webhookId = runBackendStepInput.getValueInteger("webhookId"); - if(webhookId != null) - { - return getQueryFilterForSingleWebhook(webhookId); - } - - return super.getQueryFilter(runBackendStepInput); - } - - - - /*************************************************************************** - * - ***************************************************************************/ - private static QQueryFilter getQueryFilterForSingleWebhook(Integer webhookId) throws QException - { - QRecord webhookRecord = GetAction.execute(Webhook.TABLE_NAME, webhookId); - if(webhookRecord == null) - { - LOG.debug("Running for non-existing webhook - returning query to find no rows.", logPair("webhookId", webhookId)); - return (falseFilter); - } - - ///////////////////////////////////////////// - // only return records for active webhooks // - ///////////////////////////////////////////// - Webhook webhook = new Webhook(webhookRecord); - if(!WebhookActiveStatus.ACTIVE.getId().equals(webhook.getActiveStatusId())) - { - LOG.debug("Running for non-active webhook - returning query to find no rows.", logPair("webhookId", webhookId)); - return falseFilter; - } - - ///////////////////////////////////////////////// - // don't return records for unhealthy webhooks // - ///////////////////////////////////////////////// - if(WebhookHealthStatus.UNHEALTHY.getId().equals(webhook.getHealthStatusId())) - { - LOG.debug("Running for non-healthy webhook - returning query to find no rows.", logPair("webhookId", webhookId)); - return falseFilter; - } - - /////////////////////////////////////////////////////////////////////////////////// - // webhook_id = ? // - // AND // - // ( // - // (status = NEW) // - // OR (status IN (SENDING, AWAITING_RETRY) AND next_attempt_timestamp < now()) // - // ) // - /////////////////////////////////////////////////////////////////////////////////// - QQueryFilter filter = new QQueryFilter() - .withCriteria(new QFilterCriteria("webhookId", QCriteriaOperator.EQUALS, webhookId)); - - QQueryFilter orStatusFilters = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR); - filter.addSubFilter(orStatusFilters); - - orStatusFilters.addSubFilter(new QQueryFilter() - .withCriteria(new QFilterCriteria("eventStatusId", QCriteriaOperator.EQUALS, WebhookEventStatus.NEW.getId()))); - - orStatusFilters.addSubFilter(new QQueryFilter() - .withCriteria(new QFilterCriteria("eventStatusId", QCriteriaOperator.IN, WebhookEventStatus.SENDING.getId(), WebhookEventStatus.AWAITING_RETRY.getId())) - .withCriteria(new QFilterCriteria("nextAttemptTimestamp", QCriteriaOperator.IS_NOT_BLANK)) - .withCriteria(new QFilterCriteria("nextAttemptTimestamp", QCriteriaOperator.LESS_THAN, Instant.now()))); - - filter.addOrderBy(new QFilterOrderBy("id")); - return (filter); - } -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventLoadStep.java b/src/main/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventLoadStep.java deleted file mode 100644 index 130adab..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventLoadStep.java +++ /dev/null @@ -1,259 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.processes; - - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Stack; -import com.kingsrook.qbits.webhooks.WebhooksQBitConfig; -import com.kingsrook.qbits.webhooks.actions.WebhookEventSender; -import com.kingsrook.qbits.webhooks.actions.WebhookHealthManager; -import com.kingsrook.qbits.webhooks.model.Webhook; -import com.kingsrook.qbits.webhooks.model.WebhookEvent; -import com.kingsrook.qbits.webhooks.model.WebhookEventSendLog; -import com.kingsrook.qbits.webhooks.model.WebhookHealthStatus; -import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.logging.QLogger; -import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine; -import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface; -import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; -import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; -import com.kingsrook.qqq.backend.core.model.actions.processes.Status; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; -import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractLoadStep; -import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ProcessSummaryProviderInterface; -import com.kingsrook.qqq.backend.core.utils.CollectionUtils; -import com.kingsrook.qqq.backend.core.utils.ListingHash; -import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; -import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.IN; - - -/******************************************************************************* - ** - *******************************************************************************/ -public class SendWebhookEventLoadStep extends AbstractLoadStep implements ProcessSummaryProviderInterface -{ - private static final QLogger LOG = QLogger.getLogger(SendWebhookEventLoadStep.class); - - /////////////////////// - // most recent first // - /////////////////////// - private boolean doHealthChecks = false; - private Integer repeatedFailsToGoUnhealthy; - private Stack recentSendLogs; - - private ProcessSummaryLine okLine = new ProcessSummaryLine(Status.OK) - .withMessageSuffix(" delivered") - .withSingularPastMessage("was") - .withPluralPastMessage("were"); - - private ProcessSummaryLine failLine = new ProcessSummaryLine(Status.ERROR, "failed to deliver"); - - private ProcessSummaryLine becameUnhealthyLine = new ProcessSummaryLine(Status.WARNING) - .withMessageSuffix(" associated webhook became unhealthy.") - .withSingularPastMessage("was not sent because its") - .withPluralPastMessage("were not sent because their"); - - - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public ArrayList getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen) - { - ArrayList rs = getTransformStep().getProcessSummary(runBackendStepOutput, isForResultScreen); - - /////////////////////////////////////////////////////////////////////////// - // replace the OK and WARNING lines from the transform step with our own // - /////////////////////////////////////////////////////////////////////////// - rs.removeIf(p -> p instanceof ProcessSummaryLine psl && (psl.getStatus().equals(Status.OK) || psl.getStatus().equals(Status.WARNING))); - - if(okLine.getCount() != null && okLine.getCount() > 0) - { - rs.add(0, okLine); - } - - if(failLine.getCount() != null && failLine.getCount() > 0) - { - rs.add(0, failLine); - } - - becameUnhealthyLine.addSelfToListIfAnyCount(rs); - return (rs); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public void preRun(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException - { - repeatedFailsToGoUnhealthy = WebhooksQBitConfig.getConfigValue(config -> config.getRepeatedFailsToGoUnhealthy()); - if(repeatedFailsToGoUnhealthy != null) - { - recentSendLogs = newSendLogStack(repeatedFailsToGoUnhealthy); - doHealthChecks = true; - } - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException - { - if(runBackendStepInput.getRecords().isEmpty()) - { - return; - } - - Webhook webhook = ((SendWebhookEventTransformStep) getTransformStep()).getWebhook(); - - /////////////////////////////////////////////////////////////////////////////////////////// - // look up the last several send logs for this webhook, to seed the recentSuccesses list // - /////////////////////////////////////////////////////////////////////////////////////////// - if(doHealthChecks) - { - List recentSendLogs = QueryAction.execute(WebhookEventSendLog.TABLE_NAME, WebhookEventSendLog.class, new QQueryFilter() - .withCriteria(new QFilterCriteria("webhookId", QCriteriaOperator.EQUALS, webhook.getId())) - .withOrderBy(new QFilterOrderBy("id", false)) - .withLimit(repeatedFailsToGoUnhealthy)); - Collections.reverse(recentSendLogs); - this.recentSendLogs.addAll(recentSendLogs); - } - - /////////////////////////////////////////////////// - // look up send logs for all events in this page // - /////////////////////////////////////////////////// - List webhookEvents = runBackendStepInput.getRecordsAsEntities(WebhookEvent.class); - List eventLogs = QueryAction.execute(WebhookEventSendLog.TABLE_NAME, WebhookEventSendLog.class, new QQueryFilter( - new QFilterCriteria("webhookEventId", IN, webhookEvents.stream().map(e -> e.getId()).toList()))); - ListingHash eventLogsByEventId = CollectionUtils.listToListingHash(eventLogs, l -> l.getWebhookEventId()); - - boolean becameUnhealthy = false; - for(WebhookEvent webhookEvent : webhookEvents) - { - if(becameUnhealthy) - { - becameUnhealthyLine.incrementCountAndAddPrimaryKey(webhookEvent.getId()); - continue; - } - - try - { - boolean lastSuccessful = newWebhookEventSender().handleEvent(webhookEvent, webhook, eventLogsByEventId.getOrDefault(webhookEvent.getId(), Collections.emptyList())); - - if(lastSuccessful) - { - okLine.incrementCountAndAddPrimaryKey(webhookEvent.getId()); - } - else - { - failLine.incrementCountAndAddPrimaryKey(webhookEvent.getId()); - } - - //////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // if we're to update health, then add this last-successful boolean to the stack, and call the health manager // - //////////////////////////////////////////////////////////////////////////////////////////////////////////////// - if(doHealthChecks) - { - recentSendLogs.push(new WebhookEventSendLog() - .withSuccessful(lastSuccessful)); - webhook = new WebhookHealthManager().updateWebhookHealth(recentSendLogs, webhook); - } - - if(WebhookHealthStatus.UNHEALTHY.getId().equals(webhook.getHealthStatusId())) - { - LOG.info("Webhook is now unhealthy - not attempting any more sends.", logPair("id", webhook.getId())); - becameUnhealthy = true; - } - } - catch(Exception e) - { - LOG.warn("Error handing webhook event", e, logPair("id", webhookEvent.getId())); - } - } - } - - - - /*************************************************************************** - ** meant so class can be overridden to use a different sender - ***************************************************************************/ - protected WebhookEventSender newWebhookEventSender() - { - return new WebhookEventSender(); - } - - - /*************************************************************************** - * meant for tests - to make a stack of send log events - ***************************************************************************/ - public Stack newSendLogStack(int maxSize) - { - return (new FixedStack<>(maxSize)); - } - - - /*************************************************************************** - * - ***************************************************************************/ - static class FixedStack extends Stack - { - private final int maxSize; - - - - /*************************************************************************** - * - ***************************************************************************/ - public FixedStack(int maxSize) - { - this.maxSize = maxSize; - } - - - - /*************************************************************************** - * - ***************************************************************************/ - @Override - public T push(T item) - { - if(size() >= maxSize) - { - remove(0); // remove bottom element - } - return super.push(item); // then push on top - } - } -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventProcessMetaDataProducer.java b/src/main/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventProcessMetaDataProducer.java deleted file mode 100644 index 3fa2710..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventProcessMetaDataProducer.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.processes; - - -import java.util.List; -import com.kingsrook.qbits.webhooks.model.Webhook; -import com.kingsrook.qbits.webhooks.model.WebhookEvent; -import com.kingsrook.qbits.webhooks.model.WebhookEventStatus; -import com.kingsrook.qbits.webhooks.model.WebhookSubscription; -import com.kingsrook.qbits.webhooks.registry.WebhookEventTypePossibleValueSource; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducer; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; -import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; -import com.kingsrook.qqq.backend.core.model.tables.QQQTable; -import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess; - - -/******************************************************************************* - ** Meta Data Producer for SendWebhookEventProcess - *******************************************************************************/ -public class SendWebhookEventProcessMetaDataProducer extends MetaDataProducer -{ - public static final String NAME = "SendWebhookEvent"; - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Override - public QProcessMetaData produce(QInstance qInstance) throws QException - { - QProcessMetaData processMetaData = StreamedETLWithFrontendProcess.processMetaDataBuilder() - .withName(NAME) - .withTableName(WebhookEvent.TABLE_NAME) - .withIcon(new QIcon().withName("send")) - .withExtractStepClass(SendWebhookEventExtractStep.class) - .withTransformStepClass(SendWebhookEventTransformStep.class) - .withLoadStepClass(SendWebhookEventLoadStep.class) - .withSourceTable(WebhookEvent.TABLE_NAME) - .withDestinationTable(WebhookEvent.TABLE_NAME) - .withPreviewMessage(StreamedETLWithFrontendProcess.DEFAULT_PREVIEW_MESSAGE_PREFIX + " considered for sending") - .withReviewStepRecordFields(List.of( - new QFieldMetaData("id", QFieldType.INTEGER), - new QFieldMetaData("webhookId", QFieldType.INTEGER).withPossibleValueSourceName(Webhook.TABLE_NAME), - new QFieldMetaData("webhookSubscriptionId", QFieldType.INTEGER).withPossibleValueSourceName(WebhookSubscription.TABLE_NAME), - new QFieldMetaData("webhookEventTypeName", QFieldType.STRING).withLabel("Event Type").withPossibleValueSourceName(WebhookEventTypePossibleValueSource.NAME), - new QFieldMetaData("eventSourceRecordQqqTableId", QFieldType.INTEGER).withLabel("Event Source Record Table").withPossibleValueSourceName(QQQTable.TABLE_NAME), - new QFieldMetaData("eventSourceRecordId", QFieldType.INTEGER), - WebhookEvent.TableMetaDataCustomizer.addEventStatusChipAdornment(new QFieldMetaData("eventStatusId", QFieldType.INTEGER).withPossibleValueSourceName(WebhookEventStatus.NAME)) - )) - .withInputFieldDefaultValue(StreamedETLWithFrontendProcess.FIELD_INCLUDE_ASSOCIATIONS, true) - .getProcessMetaData(); - - return (processMetaData); - } - -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventTransformStep.java b/src/main/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventTransformStep.java deleted file mode 100644 index b17570a..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventTransformStep.java +++ /dev/null @@ -1,312 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.processes; - - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; -import com.kingsrook.qbits.webhooks.model.Webhook; -import com.kingsrook.qbits.webhooks.model.WebhookActiveStatus; -import com.kingsrook.qbits.webhooks.model.WebhookEvent; -import com.kingsrook.qbits.webhooks.model.WebhookEventStatus; -import com.kingsrook.qbits.webhooks.model.WebhookHealthStatus; -import com.kingsrook.qbits.webhooks.model.WebhookSubscription; -import com.kingsrook.qqq.backend.core.actions.tables.GetAction; -import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine; -import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface; -import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; -import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; -import com.kingsrook.qqq.backend.core.model.actions.processes.Status; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep; - - -/******************************************************************************* - ** - *******************************************************************************/ -public class SendWebhookEventTransformStep extends AbstractTransformStep -{ - private Integer webhookId; - private Webhook webhook; - - private Map subscriptionMap = new HashMap<>(); - - private ProcessSummaryLine okLine = new ProcessSummaryLine(Status.OK) - .withMessageSuffix(" sent") - .withSingularFutureMessage("will be") - .withPluralFutureMessage("will be") - .withSingularPastMessage("was") - .withPluralPastMessage("were"); - - private ProcessSummaryLine differentWebhookLine = new ProcessSummaryLine(Status.ERROR) - .withMessageSuffix(" sent because events were found for more than one webhook") - .withSingularFutureMessage("will not be") - .withPluralFutureMessage("will not be") - .withSingularPastMessage("was not") - .withPluralPastMessage("were not"); - - private ProcessSummaryLine missingWebhookLine = new ProcessSummaryLine(Status.ERROR) - .withMessageSuffix(" associated webhook was not found") - .withSingularFutureMessage("will not be sent because its") - .withPluralFutureMessage("will not be sent because their") - .withSingularPastMessage("was not sent because its") - .withPluralPastMessage("were not sent because their"); - - private ProcessSummaryLine disabledSubscriptionLine = new ProcessSummaryLine(Status.ERROR) - .withSingularFutureMessage("will not be sent because its associated subscription is disabled") - .withPluralFutureMessage("will not be sent because their associated subscriptions are disabled") - .withSingularPastMessage("was not sent because its associated subscription is disabled") - .withPluralPastMessage("were not sent because their associated subscriptions are disabled"); - - private ProcessSummaryLine disabledWebhookLine = new ProcessSummaryLine(Status.ERROR) - .withSingularFutureMessage("will not be sent because its associated webhook is disabled") - .withPluralFutureMessage("will not be sent because their associated webhooks disabled") - .withSingularPastMessage("was not sent because its associated webhook is disabled") - .withPluralPastMessage("were not sent because their associated webhooks are disabled"); - - private ProcessSummaryLine unhealthyWebhookLine = new ProcessSummaryLine(Status.ERROR) - .withSingularFutureMessage("will not be sent because its associated webhook is unhealthy") - .withPluralFutureMessage("will not be sent because their associated webhooks unhealthy") - .withSingularPastMessage("was not sent because its associated webhook is unhealthy") - .withPluralPastMessage("were not sent because their associated webhooks are unhealthy"); - - private ProcessSummaryLine missingSubscriptionLine = new ProcessSummaryLine(Status.ERROR) - .withMessageSuffix(" associated subscription was not found") - .withSingularFutureMessage("will not be sent because its") - .withPluralFutureMessage("will not be sent because their") - .withSingularPastMessage("was not sent because its") - .withPluralPastMessage("were not sent because their"); - - private ProcessSummaryLine alreadyDeliveredWarningLine = new ProcessSummaryLine(Status.WARNING) - .withSingularFutureMessage("was already successfully delivered, but will be sent again") - .withPluralFutureMessage("were already successfully delivered, but will be sent again") - .withSingularPastMessage("was already successfully delivered, but was sent again") - .withPluralPastMessage("were already successfully delivered, but were sent again"); - - - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public void preRun(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException - { - //////////////////////////////////////////////////////////////// - // capture if we're running for a single specified webhook Id // - //////////////////////////////////////////////////////////////// - webhookId = runBackendStepInput.getValueInteger("webhookId"); - super.preRun(runBackendStepInput, runBackendStepOutput); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public ArrayList getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen) - { - ArrayList rs = new ArrayList<>(); - okLine.addSelfToListIfAnyCount(rs); - alreadyDeliveredWarningLine.addSelfToListIfAnyCount(rs); - differentWebhookLine.addSelfToListIfAnyCount(rs); - missingWebhookLine.addSelfToListIfAnyCount(rs); - missingSubscriptionLine.addSelfToListIfAnyCount(rs); - disabledWebhookLine.addSelfToListIfAnyCount(rs); - unhealthyWebhookLine.addSelfToListIfAnyCount(rs); - disabledSubscriptionLine.addSelfToListIfAnyCount(rs); - return rs; - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException - { - //////////////////////////////////////////////////// - // fetch the subscriptions for the webhook events // - //////////////////////////////////////////////////// - List webhookEvents = runBackendStepInput.getRecordsAsEntities(WebhookEvent.class); - Set subscriptionIds = webhookEvents.stream().map(we -> we.getWebhookSubscriptionId()).collect(Collectors.toSet()); - loadSubscriptions(subscriptionIds); - - for(WebhookEvent webhookEvent : webhookEvents) - { - if(webhookEvent.getWebhookId() == null) - { - ////////////////////////////////////////////////// - // a little bit of a misnomer, but close enough // - ////////////////////////////////////////////////// - missingWebhookLine.incrementCountAndAddPrimaryKey(webhookEvent.getId()); - continue; - } - - if(webhookEvent.getWebhookSubscriptionId() == null || !subscriptionIds.contains(webhookEvent.getWebhookSubscriptionId())) - { - missingSubscriptionLine.incrementCountAndAddPrimaryKey(webhookEvent.getId()); - continue; - } - - ///////////////////////////////////////////////////////////////////////////////////////////// - // if we aren't running for a specific webhook, then grab the id off the first one we find // - ///////////////////////////////////////////////////////////////////////////////////////////// - if(webhookId == null) - { - webhookId = webhookEvent.getWebhookId(); - } - - ////////////////////////////////////////////////////////////////////////////////// - // in case events from multiple webhooks were found, only allow them for one // - // (the first one we found). this will simplify sending, at least at this time. // - ////////////////////////////////////////////////////////////////////////////////// - if(!Objects.equals(webhookId, webhookEvent.getWebhookId())) - { - differentWebhookLine.incrementCountAndAddPrimaryKey(webhookEvent.getId()); - continue; - } - - /////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // if we haven't loaded the webhook record yet, then do so now (should be on the first/only one we're using) // - /////////////////////////////////////////////////////////////////////////////////////////////////////////////// - if(webhook == null && webhookId != null) - { - QRecord webhookRecord = GetAction.execute(Webhook.TABLE_NAME, webhookId); - if(webhookRecord == null) - { - ///////////////////////////////////////////////////////////////////////////////////////// - // if that webhook is missing, skip this line. // - // note, if we had many records selected, all for a non-existing webhook, we'd do this // - // query a bunch, but, not too concerned about that scenario realistically happening. // - ///////////////////////////////////////////////////////////////////////////////////////// - missingWebhookLine.incrementCountAndAddPrimaryKey(webhookEvent.getId()); - continue; - } - - webhook = new Webhook(webhookRecord); - } - - if(webhook == null) - { - ////////////////////////////////////////////////////////////////////////////////////////////// - // I think this is redundant, but it stops compiler warnings about webhook maybe being null // - ////////////////////////////////////////////////////////////////////////////////////////////// - missingWebhookLine.incrementCountAndAddPrimaryKey(webhookEvent.getId()); - continue; - } - - if(Objects.equals(WebhookActiveStatus.DISABLED.getId(), webhook.getActiveStatusId())) - { - disabledWebhookLine.incrementCountAndAddPrimaryKey(webhookEvent.getId()); - continue; - } - - if(Objects.equals(WebhookHealthStatus.UNHEALTHY.getId(), webhook.getHealthStatusId())) - { - unhealthyWebhookLine.incrementCountAndAddPrimaryKey(webhookEvent.getId()); - continue; - } - - WebhookSubscription webhookSubscription = subscriptionMap.get(webhookEvent.getWebhookSubscriptionId()); - if(webhookSubscription == null) - { - missingSubscriptionLine.incrementCountAndAddPrimaryKey(webhookEvent.getId()); - continue; - } - - if(Objects.equals(WebhookActiveStatus.DISABLED.getId(), webhookSubscription.getActiveStatusId())) - { - disabledSubscriptionLine.incrementCountAndAddPrimaryKey(webhookEvent.getId()); - continue; - } - - if(WebhookEventStatus.DELIVERED.getId().equals(webhookEvent.getEventStatusId())) - { - /////////////////////////////////////////////////////////////////////// - // already delivered gets a warning, but still proceeds to load step // - /////////////////////////////////////////////////////////////////////// - alreadyDeliveredWarningLine.incrementCountAndAddPrimaryKey(webhookEvent.getId()); - runBackendStepOutput.addRecordEntity(webhookEvent); - } - else - { - /////////////////////////////////////////////////////// - // finally all ok - move the record to the load step // - /////////////////////////////////////////////////////// - okLine.incrementCountAndAddPrimaryKey(webhookEvent.getId()); - runBackendStepOutput.addRecordEntity(webhookEvent); - } - } - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - private void loadSubscriptions(Set subscriptionIds) throws QException - { - Set subscriptionsToLookup = new HashSet<>(subscriptionIds); - subscriptionsToLookup.removeAll(subscriptionMap.keySet()); - if(!subscriptionsToLookup.isEmpty()) - { - List subscriptions = QueryAction.execute(WebhookSubscription.TABLE_NAME, WebhookSubscription.class, new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.IN, subscriptionsToLookup))); - for(WebhookSubscription subscription : subscriptions) - { - subscriptionMap.put(subscription.getId(), subscription); - } - } - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - Webhook getWebhook() - { - return webhook; - } - - - - /*************************************************************************** - ** originally meant for tests - ***************************************************************************/ - SendWebhookEventTransformStep withWebhook(Webhook webhook) - { - this.webhook = webhook; - return this; - } -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/processes/SyncWebhookScheduledJobProcess.java b/src/main/java/com/kingsrook/qbits/webhooks/processes/SyncWebhookScheduledJobProcess.java deleted file mode 100644 index 70871de..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/processes/SyncWebhookScheduledJobProcess.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.processes; - - -import com.kingsrook.qbits.webhooks.WebhooksQBitConfig; -import com.kingsrook.qbits.webhooks.model.Webhook; -import com.kingsrook.qbits.webhooks.model.WebhookActiveStatus; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJob; -import com.kingsrook.qqq.backend.core.scheduler.processes.AbstractRecordSyncToScheduledJobProcess; -import com.kingsrook.qqq.backend.core.utils.StringUtils; - - -/******************************************************************************* - ** - *******************************************************************************/ -public class SyncWebhookScheduledJobProcess extends AbstractRecordSyncToScheduledJobProcess -{ - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - protected ScheduledJob customizeScheduledJob(ScheduledJob scheduledJob, QRecord sourceRecord) throws QException - { - scheduledJob.setRepeatSeconds(WebhooksQBitConfig.getConfigValue(config -> config.getSendWebhookEventProcessRepeatSeconds())); - - boolean isActive = (WebhookActiveStatus.ACTIVE.getId().equals(sourceRecord.getValueInteger("activeStatusId"))); - scheduledJob.setIsActive(isActive); - - if(!StringUtils.hasContent(scheduledJob.getSchedulerName())) - { - scheduledJob.setSchedulerName(WebhooksQBitConfig.getConfigValue(config -> config.getSchedulerName())); - } - - return (scheduledJob); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - protected String getScheduledJobForeignKeyType() - { - return (Webhook.TABLE_NAME); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - protected String getRecordForeignKeyFieldName() - { - return ("id"); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - protected String getRecordForeignKeyPossibleValueSourceName() - { - return (Webhook.TABLE_NAME); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - protected String getSourceTableName() - { - return (Webhook.TABLE_NAME); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - protected String getProcessNameScheduledJobParameter() - { - return (SendWebhookEventProcessMetaDataProducer.NAME); - } -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/registry/WebhooksRegistryEnricherPlugin.java b/src/main/java/com/kingsrook/qbits/webhooks/registry/WebhooksRegistryEnricherPlugin.java deleted file mode 100644 index 2a17f2b..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/registry/WebhooksRegistryEnricherPlugin.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.registry; - - -import com.kingsrook.qqq.backend.core.instances.enrichment.plugins.QInstanceEnricherPluginInterface; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; - - -/******************************************************************************* - ** - *******************************************************************************/ -public class WebhooksRegistryEnricherPlugin implements QInstanceEnricherPluginInterface -{ - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public void enrich(QInstance qInstance, QInstance qInstance2) - { - } -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/registry/WebhooksRegistryValidatorPlugin.java b/src/main/java/com/kingsrook/qbits/webhooks/registry/WebhooksRegistryValidatorPlugin.java deleted file mode 100644 index f22b419..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/registry/WebhooksRegistryValidatorPlugin.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.registry; - - -import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; -import com.kingsrook.qqq.backend.core.instances.validation.plugins.QInstanceValidatorPluginInterface; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; - - -/******************************************************************************* - ** - *******************************************************************************/ -public class WebhooksRegistryValidatorPlugin implements QInstanceValidatorPluginInterface -{ - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public void validate(QInstance qInstance, QInstance qInstance2, QInstanceValidator validator) - { - } -} diff --git a/src/main/java/com/kingsrook/qbits/webhooks/tables/WebhookEventTableCustomizer.java b/src/main/java/com/kingsrook/qbits/webhooks/tables/WebhookEventTableCustomizer.java deleted file mode 100644 index 4405aa2..0000000 --- a/src/main/java/com/kingsrook/qbits/webhooks/tables/WebhookEventTableCustomizer.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.tables; - - -import java.util.List; -import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.model.tables.QQQTableTableManager; - - -/******************************************************************************* - ** - *******************************************************************************/ -public class WebhookEventTableCustomizer implements TableCustomizerInterface -{ - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public List postQuery(QueryOrGetInputInterface queryInput, List records) throws QException - { - return QQQTableTableManager.setRecordLinksToRecordsFromTableDynamicForPostQuery(queryInput, records, "eventSourceRecordQqqTableId", "eventSourceRecordId"); - } - -} diff --git a/src/test/java/com/kingsrook/qbits/webhooks/BaseTest.java b/src/test/java/com/kingsrook/qbits/webhooks/BaseTest.java deleted file mode 100644 index d044a00..0000000 --- a/src/test/java/com/kingsrook/qbits/webhooks/BaseTest.java +++ /dev/null @@ -1,244 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks; - - -import java.io.Serializable; -import java.util.List; -import java.util.Random; -import java.util.stream.Collectors; -import com.kingsrook.qbits.webhooks.actions.WebhookSubscriptionsHelper; -import com.kingsrook.qbits.webhooks.model.Webhook; -import com.kingsrook.qbits.webhooks.model.WebhookActiveStatus; -import com.kingsrook.qbits.webhooks.model.WebhookEvent; -import com.kingsrook.qbits.webhooks.model.WebhookEventCategory; -import com.kingsrook.qbits.webhooks.model.WebhookEventStatus; -import com.kingsrook.qbits.webhooks.model.WebhookHealthStatus; -import com.kingsrook.qbits.webhooks.model.WebhookSubscription; -import com.kingsrook.qbits.webhooks.registry.WebhookEventType; -import com.kingsrook.qbits.webhooks.registry.WebhooksRegistry; -import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; -import com.kingsrook.qqq.backend.core.context.QContext; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; -import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.session.QSession; -import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import static org.junit.jupiter.api.Assertions.fail; - - -/******************************************************************************* - ** - *******************************************************************************/ -public class BaseTest -{ - protected static Random random = new Random(); - - - - /******************************************************************************* - ** - *******************************************************************************/ - @BeforeEach - void baseBeforeEach() throws Exception - { - QInstance qInstance = defineQInstance(); - new QInstanceValidator().validate(qInstance); - QContext.init(qInstance, new QSession().withSecurityKeyValue(WebhooksTestApplication.STORE_ID_ALL_ACCESS_KEY, true)); - WebhookSubscriptionsHelper.clearMemoizations(); - - MemoryRecordStore.fullReset(); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @AfterEach - void afterEach() - { - MemoryRecordStore.fullReset(); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - private QInstance defineQInstance() throws QException - { - return new WebhooksTestApplication().defineQInstance(); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static void registerAdHocEventType(String name) throws QException - { - WebhooksRegistry.ofOrWithNew(QContext.getQInstance()).registerWebhookEventType(new WebhookEventType() - .withName(name).withCategory(WebhookEventCategory.AD_HOC)); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static void registerEventType(String name, WebhookEventCategory category, String tableName) throws QException - { - WebhooksRegistry.ofOrWithNew(QContext.getQInstance()).registerWebhookEventType(new WebhookEventType() - .withName(name).withCategory(category).withTableName(tableName)); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static void registerEventType(String name, WebhookEventCategory category, String tableName, String fieldName) throws QException - { - WebhooksRegistry.ofOrWithNew(QContext.getQInstance()).registerWebhookEventType(new WebhookEventType() - .withName(name).withCategory(category).withTableName(tableName).withFieldName(fieldName)); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static void registerEventType(String name, WebhookEventCategory category, String tableName, String fieldName, Serializable value) throws QException - { - WebhooksRegistry.ofOrWithNew(QContext.getQInstance()).registerWebhookEventType(new WebhookEventType() - .withName(name).withCategory(category).withTableName(tableName).withFieldName(fieldName).withValue(value)); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static Integer insert(Webhook webhook) throws QException - { - Integer id = new InsertAction().execute(new InsertInput(Webhook.TABLE_NAME) - .withRecordEntity(webhook)).getRecords().get(0).getValueInteger("id"); - if(id == null) - { - fail("Failed to insert a test webhook"); - } - return (id); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static Webhook newWebhook(String name) - { - return new Webhook() - .withId(random.nextInt(100000)) - .withName(name) - .withUrl("http://localhost/") - .withActiveStatusId(WebhookActiveStatus.ACTIVE.getId()) - .withHealthStatusId(WebhookHealthStatus.HEALTHY.getId()); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static Integer insert(WebhookSubscription subscription) throws QException - { - Integer id = new InsertAction().execute(new InsertInput(WebhookSubscription.TABLE_NAME) - .withRecordEntity(subscription)).getRecords().get(0).getValueInteger("id"); - if(id == null) - { - fail("Failed to insert a test subscription"); - } - return (id); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static WebhookSubscription newWebhookSubscription(String eventTypeName) - { - return new WebhookSubscription() - .withId(random.nextInt(100000)) - .withWebhookEventTypeName(eventTypeName) - .withApiName(WebhooksTestApplication.API_NAME) - .withApiVersion(WebhooksTestApplication.API_V1) - .withWebhookId(-1); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static Integer insert(WebhookEvent event) throws QException - { - return insert(List.of(event)).get(0); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static List insert(List events) throws QException - { - List ids = new InsertAction().execute(new InsertInput(WebhookEvent.TABLE_NAME) - .withRecordEntities(events)).getRecords().stream().map(qr -> qr.getValueInteger("id")).collect(Collectors.toList()); - - if(ids.contains(null)) - { - fail("Failed to insert a test event"); - } - return (ids); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static WebhookEvent newWebhookEvent(WebhookSubscription subscription, String eventTypeName) throws QException - { - WebhookEvent webhookEvent = new WebhookEvent(); - webhookEvent.setWebhookId(subscription.getWebhookId()); - webhookEvent.setWebhookSubscriptionId(subscription.getId()); - webhookEvent.setWebhookEventTypeName(subscription.getWebhookEventTypeName()); - webhookEvent.setEventStatusId(WebhookEventStatus.NEW.getId()); - return (webhookEvent); - } - -} - diff --git a/src/test/java/com/kingsrook/qbits/webhooks/WebhooksQBitConfigTest.java b/src/test/java/com/kingsrook/qbits/webhooks/WebhooksQBitConfigTest.java deleted file mode 100644 index 3a31850..0000000 --- a/src/test/java/com/kingsrook/qbits/webhooks/WebhooksQBitConfigTest.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks; - - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.function.Consumer; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; - - -/******************************************************************************* - ** Unit test for WebhooksQBitConfig - *******************************************************************************/ -class WebhooksQBitConfigTest extends BaseTest -{ - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testValidation() - { - ///////////////////////////////////////////// - // see corresponding todo in the main code // - ///////////////////////////////////////////// - /* - /////////////////////////////////////////////////////////////////////// - // defaultBackendNameForTables is optional, but if given, must exist // - /////////////////////////////////////////////////////////////////////// - assertSuccess(new WebhooksQBitConfig().withDefaultBackendNameForTables(null)); - assertSuccess(new WebhooksQBitConfig().withDefaultBackendNameForTables("myBackend"), qInstance -> qInstance.addBackend(new QBackendMetaData().withName("myBackend"))); - assertError("Unrecognized defaultBackendNameForTables: noSuchBackend", new WebhooksQBitConfig().withDefaultBackendNameForTables("noSuchBackend")); - - ////////////////////////////////////////////////////////// - // scheduler name is optional, but if given, must exist // - ////////////////////////////////////////////////////////// - assertSuccess(new WebhooksQBitConfig().withSchedulerName(null)); - assertSuccess(new WebhooksQBitConfig().withSchedulerName("myScheduler"), qInstance -> qInstance.addScheduler(new SimpleSchedulerMetaData().withName("myScheduler"))); - assertError("Unrecognized schedulerName: noSuchScheduler", new WebhooksQBitConfig().withSchedulerName("noSuchScheduler")); - */ - - //////////////////////////////////////////////////////////////////////// - // sendWebhookEventProcessRepeatSeconds - cannot be null, must be > 0 // - //////////////////////////////////////////////////////////////////////// - assertSuccess(new WebhooksQBitConfig().withSendWebhookEventProcessRepeatSeconds(1)); - assertError("sendWebhookEventProcessRepeatSeconds must be non-null and > 0", new WebhooksQBitConfig().withSendWebhookEventProcessRepeatSeconds(null)); - assertError("sendWebhookEventProcessRepeatSeconds must be non-null and > 0", new WebhooksQBitConfig().withSendWebhookEventProcessRepeatSeconds(-1)); - assertError("sendWebhookEventProcessRepeatSeconds must be non-null and > 0", new WebhooksQBitConfig().withSendWebhookEventProcessRepeatSeconds(0)); - - //////////////////////////////////////////////////////// - // repeatedFailsToGoUnhealthy - can be null, else > 0 // - //////////////////////////////////////////////////////// - assertSuccess(new WebhooksQBitConfig().withRepeatedFailsToGoUnhealthy(null)); - assertSuccess(new WebhooksQBitConfig().withRepeatedFailsToGoUnhealthy(5)); - assertError("If repeatedFailsToGoUnhealthy is given, it must be > 0", new WebhooksQBitConfig().withRepeatedFailsToGoUnhealthy(-1)); - assertError("If repeatedFailsToGoUnhealthy is given, it must be > 0", new WebhooksQBitConfig().withRepeatedFailsToGoUnhealthy(0)); - - //////////////////////////////////////////////////////////////// - // unhealthyToProbationTimeoutMinutes - can be null, else > 0 // - //////////////////////////////////////////////////////////////// - assertSuccess(new WebhooksQBitConfig().withUnhealthyToProbationTimeoutMinutes(null)); - assertSuccess(new WebhooksQBitConfig().withUnhealthyToProbationTimeoutMinutes(5)); - assertError("If unhealthyToProbationTimeoutMinutes is given, it must be > 0", new WebhooksQBitConfig().withUnhealthyToProbationTimeoutMinutes(-1)); - assertError("If unhealthyToProbationTimeoutMinutes is given, it must be > 0", new WebhooksQBitConfig().withUnhealthyToProbationTimeoutMinutes(0)); - - ///////////////////////////////////////////////////////////// - // memoizationTimeoutMinutes - cannot be null, must be > 0 // - ///////////////////////////////////////////////////////////// - assertSuccess(new WebhooksQBitConfig().withMemoizationTimeoutMinutes(1)); - assertSuccess(new WebhooksQBitConfig().withMemoizationTimeoutMinutes(0)); - assertError("memoizationTimeoutMinutes must be non-null and >= 0", new WebhooksQBitConfig().withMemoizationTimeoutMinutes(null)); - assertError("memoizationTimeoutMinutes must be non-null and >= 0", new WebhooksQBitConfig().withMemoizationTimeoutMinutes(-1)); - - ///////////////////////////////////////////////////////////////// - // maxSendAttemptsBeforeFailure - cannot be null, must be >= 1 // - ///////////////////////////////////////////////////////////////// - assertSuccess(new WebhooksQBitConfig().withMaxSendAttemptsBeforeFailure(1)); - assertError("maxSendAttemptsBeforeFailure must be non-null and >= 1", new WebhooksQBitConfig().withMaxSendAttemptsBeforeFailure(null)); - assertError("maxSendAttemptsBeforeFailure must be non-null and >= 1", new WebhooksQBitConfig().withMaxSendAttemptsBeforeFailure(0)); - assertError("maxSendAttemptsBeforeFailure must be non-null and >= 1", new WebhooksQBitConfig().withMaxSendAttemptsBeforeFailure(-1)); - - ////////////////////////////////////////////////////////////////////// - // minutesToConsiderLeakedSendAttempt - cannot be null, must be > 0 // - ////////////////////////////////////////////////////////////////////// - assertSuccess(new WebhooksQBitConfig().withMinutesToConsiderLeakedSendAttempt(1)); - assertError("minutesToConsiderLeakedSendAttempt must be non-null and >= 1", new WebhooksQBitConfig().withMinutesToConsiderLeakedSendAttempt(null)); - assertError("minutesToConsiderLeakedSendAttempt must be non-null and >= 1", new WebhooksQBitConfig().withMinutesToConsiderLeakedSendAttempt(0)); - assertError("minutesToConsiderLeakedSendAttempt must be non-null and >= 1", new WebhooksQBitConfig().withMinutesToConsiderLeakedSendAttempt(-1)); - - ///////////////////////////////////////////////////////////////////////////////////// - // minutesBetweenRetryAttempts - cannot be null or empty; all entries must be >= 0 // - ///////////////////////////////////////////////////////////////////////////////////// - assertSuccess(new WebhooksQBitConfig().withMinutesBetweenRetryAttempts(List.of(0))); - assertSuccess(new WebhooksQBitConfig().withMinutesBetweenRetryAttempts(List.of(1))); - assertSuccess(new WebhooksQBitConfig().withMinutesBetweenRetryAttempts(List.of(1, 5, 30))); - assertError("minutesBetweenRetryAttempts must be non-null and non-empty", new WebhooksQBitConfig().withMinutesBetweenRetryAttempts(null)); - assertError("minutesBetweenRetryAttempts must be non-null and non-empty", new WebhooksQBitConfig().withMinutesBetweenRetryAttempts(Collections.emptyList())); - assertError("minutesBetweenRetryAttempts must have all values >= 0", new WebhooksQBitConfig().withMinutesBetweenRetryAttempts(List.of(-1))); - assertError("minutesBetweenRetryAttempts must have all values >= 0", new WebhooksQBitConfig().withMinutesBetweenRetryAttempts(List.of(1, 5, -1))); - - //////////////////////////////////////////////////////// - // maxRateLimitRetries - cannot be null, must be >= 0 // - //////////////////////////////////////////////////////// - assertSuccess(new WebhooksQBitConfig().withMaxRateLimitRetries(1)); - assertSuccess(new WebhooksQBitConfig().withMaxRateLimitRetries(0)); - assertError("maxRateLimitRetries must be non-null and >= 0", new WebhooksQBitConfig().withMaxRateLimitRetries(null)); - assertError("maxRateLimitRetries must be non-null and >= 0", new WebhooksQBitConfig().withMaxRateLimitRetries(-1)); - - ///////////////////////////////////////////////////////////////// - // initialRateLimitBackoffMillis - cannot be null, must be > 0 // - ///////////////////////////////////////////////////////////////// - assertSuccess(new WebhooksQBitConfig().withInitialRateLimitBackoffMillis(1)); - assertError("initialRateLimitBackoffMillis must be non-null and > 0", new WebhooksQBitConfig().withInitialRateLimitBackoffMillis(null)); - assertError("initialRateLimitBackoffMillis must be non-null and > 0", new WebhooksQBitConfig().withInitialRateLimitBackoffMillis(-1)); - assertError("initialRateLimitBackoffMillis must be non-null and > 0", new WebhooksQBitConfig().withInitialRateLimitBackoffMillis(0)); - - ///////////////////////////////////////////////////// - // httpTimeoutMillis - cannot be null, must be > 0 // - ///////////////////////////////////////////////////// - assertSuccess(new WebhooksQBitConfig().withHttpTimeoutMillis(1)); - assertError("httpTimeoutMillis must be non-null and > 0", new WebhooksQBitConfig().withHttpTimeoutMillis(null)); - assertError("httpTimeoutMillis must be non-null and > 0", new WebhooksQBitConfig().withHttpTimeoutMillis(-1)); - assertError("httpTimeoutMillis must be non-null and > 0", new WebhooksQBitConfig().withHttpTimeoutMillis(0)); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - private void assertError(String expectedErrorMessage, WebhooksQBitConfig config) - { - List errors = new ArrayList<>(); - config.validate(new QInstance(), errors); - assertEquals(1, errors.size()); - assertEquals(expectedErrorMessage, errors.get(0)); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - private void assertSuccess(WebhooksQBitConfig config) - { - assertSuccess(config, null); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - private void assertSuccess(WebhooksQBitConfig config, Consumer qInstanceConsumer) - { - List errors = new ArrayList<>(); - - QInstance qInstance = new QInstance(); - if(qInstanceConsumer != null) - { - qInstanceConsumer.accept(qInstance); - } - - config.validate(qInstance, errors); - assertEquals(0, errors.size()); - } - -} \ No newline at end of file diff --git a/src/test/java/com/kingsrook/qbits/webhooks/WebhooksTestApplication.java b/src/test/java/com/kingsrook/qbits/webhooks/WebhooksTestApplication.java deleted file mode 100644 index 22ec5ec..0000000 --- a/src/test/java/com/kingsrook/qbits/webhooks/WebhooksTestApplication.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks; - - -import java.util.List; -import com.kingsrook.qbits.webhooks.model.WebhookEventCategory; -import com.kingsrook.qbits.webhooks.registry.WebhookEventType; -import com.kingsrook.qbits.webhooks.registry.WebhooksRegistry; -import com.kingsrook.qqq.api.model.APIVersion; -import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; -import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; -import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataProvider; -import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; -import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication; -import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput; -import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; -import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel; -import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules; -import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; -import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection; -import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; -import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType; -import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; -import com.kingsrook.qqq.backend.core.model.tables.QQQTablesMetaDataProvider; -import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule; - - -/******************************************************************************* - ** - *******************************************************************************/ -public class WebhooksTestApplication extends AbstractQQQApplication -{ - public static final String MEMORY_BACKEND_NAME = "memory"; - public static final String TABLE_NAME_PERSON = "person"; - public static final String TABLE_NAME_ORDER = "order"; - - public static final String API_NAME = "test-api"; - public static final String API_PATH = "/test-api/"; - public static final String API_V1 = "v1"; - - public static final String PERSON_INSERTED_EVENT_TYPE_NAME = "person.inserted"; - public static final String PERSON_STORED_EVENT_TYPE_NAME = "person.stored"; - - public static final String STORE_ID_ALL_ACCESS_KEY = "storeIdAllAccess"; - public static final String STORE_ID_KEY = "storeId"; - - - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public QInstance defineQInstance() throws QException - { - QInstance qInstance = new QInstance(); - - qInstance.withInstanceDefaultAuthentication(new QAuthenticationMetaData().withType(QAuthenticationType.FULLY_ANONYMOUS)); - - qInstance.addBackend(new QBackendMetaData() - .withName(MEMORY_BACKEND_NAME) - .withBackendType(MemoryBackendModule.class)); - - ////////////////////////////// - // add tables table and PVS // - ////////////////////////////// - new QQQTablesMetaDataProvider().defineAll(qInstance, MEMORY_BACKEND_NAME, MEMORY_BACKEND_NAME, null); - - ////////////////////// - // produce our qbit // - ////////////////////// - WebhooksQBitProducer producer = new WebhooksQBitProducer() - .withQBitConfig(new WebhooksQBitConfig() - .withSecurityFields(List.of(new QFieldMetaData("storeId", QFieldType.INTEGER))) - .withRecordSecurityLocks(List.of(new RecordSecurityLock().withFieldName("storeId").withSecurityKeyType(STORE_ID_KEY))) - .withDefaultBackendNameForTables(MEMORY_BACKEND_NAME)); - - MetaDataProducerMultiOutput allQBitMetaData = producer.produce(qInstance); - allQBitMetaData.addSelfToInstance(qInstance); - - ////////////////////////////// - // add a webhook event type // - ////////////////////////////// - WebhooksRegistry.ofOrWithNew(qInstance).registerWebhookEventType(new WebhookEventType() - .withName(PERSON_INSERTED_EVENT_TYPE_NAME) - .withLabel("Person Inserted") - .withCategory(WebhookEventCategory.INSERT) - .withTableName(TABLE_NAME_PERSON)); - - WebhooksRegistry.ofOrWithNew(qInstance).registerWebhookEventType(new WebhookEventType() - .withName(PERSON_STORED_EVENT_TYPE_NAME) - .withLabel("Person Stored") - .withCategory(WebhookEventCategory.STORE) - .withTableName(TABLE_NAME_PERSON)); - - ///////////// - // add api // - ///////////// - defineApiMetaData(qInstance); - ApiInstanceMetaDataProvider.definePossibleValueSourcesForApiNameAndVersion(qInstance); - - //////////////////////////////////////// - // produce some test tables // - // note - nice to be a qbit this too! // - //////////////////////////////////////// - qInstance.addTable(new QTableMetaData() - .withName(TABLE_NAME_PERSON) - .withBackendName(MEMORY_BACKEND_NAME) - .withPrimaryKeyField("id") - .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) - .withField(new QFieldMetaData("firstName", QFieldType.STRING).withIsEditable(true)) - .withField(new QFieldMetaData("lastName", QFieldType.STRING).withIsEditable(true)) - .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(API_NAME, new ApiTableMetaData() - .withInitialVersion(API_V1))) - ); - - qInstance.addTable(new QTableMetaData() - .withName(TABLE_NAME_ORDER) - .withBackendName(MEMORY_BACKEND_NAME) - .withPrimaryKeyField("id") - .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) - .withField(new QFieldMetaData("orderNo", QFieldType.STRING).withIsEditable(true)) - .withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withIsEditable(true)) - .withRecordSecurityLock(new RecordSecurityLock().withFieldName("storeId").withSecurityKeyType(STORE_ID_KEY)) - .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(API_NAME, new ApiTableMetaData() - .withInitialVersion(API_V1))) - ); - - qInstance.addSecurityKeyType(new QSecurityKeyType().withName(STORE_ID_KEY).withAllAccessKeyName(STORE_ID_ALL_ACCESS_KEY)); - - /////////////////////////////////////////// - // turn off audits (why on by default??) // - /////////////////////////////////////////// - qInstance.getTables().values().forEach(t -> t.setAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))); - - ///////////////// - // create apps // - ///////////////// - qInstance.addApp(new QAppMetaData() - .withName("webhooks") - .withIcon(new QIcon("webhooks")) - .withSection(WebhooksQBitProducer.produceAppSection())); - - qInstance.addApp(new QAppMetaData() - .withName("testData") - .withIcon(new QIcon("dataset")) - .withSection(new QAppSection() - .withName("people") - .withTables(List.of(TABLE_NAME_PERSON)))); - - return qInstance; - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - private void defineApiMetaData(QInstance qInstance) - { - qInstance.withSupplementalMetaData(new ApiInstanceMetaDataContainer() - .withApiInstanceMetaData(new ApiInstanceMetaData() - .withName(API_NAME) - .withPath(API_PATH) - .withLabel("Test API") - .withDescription("QQQ Test API") - .withContactEmail("contact@kingsrook.com") - .withCurrentVersion(new APIVersion(API_V1)) - .withSupportedVersions(List.of(new APIVersion(API_V1))) - )); - } - -} diff --git a/src/test/java/com/kingsrook/qbits/webhooks/WebhooksTestJavalinServer.java b/src/test/java/com/kingsrook/qbits/webhooks/WebhooksTestJavalinServer.java deleted file mode 100644 index 40f36cb..0000000 --- a/src/test/java/com/kingsrook/qbits/webhooks/WebhooksTestJavalinServer.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks; - - -import java.util.List; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.middleware.javalin.QApplicationJavalinServer; -import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1; - - -/******************************************************************************* - ** - *******************************************************************************/ -public class WebhooksTestJavalinServer -{ - - /*************************************************************************** - ** - ***************************************************************************/ - public static void main(String[] args) - { - startApplicationJavalinServer(); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - private static void startApplicationJavalinServer() - { - try - { - System.setProperty("qqq.javalin.hotSwapInstance", "true"); - - QApplicationJavalinServer javalinServer = new QApplicationJavalinServer(new WebhooksTestApplication()) - .withMiddlewareVersionList(List.of(new MiddlewareVersionV1())) - .withServeFrontendMaterialDashboard(false); - javalinServer.start(); - } - catch(QException e) - { - e.printStackTrace(); - } - } - -} diff --git a/src/test/java/com/kingsrook/qbits/webhooks/WebhooksTestReceiverApplication.java b/src/test/java/com/kingsrook/qbits/webhooks/WebhooksTestReceiverApplication.java deleted file mode 100644 index c30d36e..0000000 --- a/src/test/java/com/kingsrook/qbits/webhooks/WebhooksTestReceiverApplication.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks; - - -import java.time.Instant; -import java.util.List; -import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; -import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication; -import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; -import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; -import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; -import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; -import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection; -import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; -import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule; -import com.kingsrook.qqq.middleware.javalin.QJavalinMetaData; -import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData; -import com.kingsrook.qqq.middleware.javalin.routeproviders.ProcessBasedRouterPayload; - - -/******************************************************************************* - ** - *******************************************************************************/ -public class WebhooksTestReceiverApplication extends AbstractQQQApplication -{ - public static final String MEMORY_BACKEND_NAME = "memory"; - public static final String RECEIPT_TABLE_NAME = "receipts"; - public static final String RECEIVE_PROCESS_NAME = "receiveWebhook"; - - - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public QInstance defineQInstance() throws QException - { - QInstance qInstance = new QInstance(); - - qInstance.withInstanceDefaultAuthentication(new QAuthenticationMetaData().withType(QAuthenticationType.FULLY_ANONYMOUS)); - - qInstance.addBackend(new QBackendMetaData() - .withName(MEMORY_BACKEND_NAME) - .withBackendType(MemoryBackendModule.class)); - - qInstance.addTable(new QTableMetaData() - .withName(RECEIPT_TABLE_NAME) - .withBackendName(MEMORY_BACKEND_NAME) - .withPrimaryKeyField("id") - .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) - .withField(new QFieldMetaData("timestamp", QFieldType.DATE_TIME).withIsEditable(true)) - .withField(new QFieldMetaData("content", QFieldType.STRING).withIsEditable(true))); - - qInstance.addProcess(new QProcessMetaData() - .withName(RECEIVE_PROCESS_NAME) - .withStep(new QBackendStepMetaData() - .withName("execute") - .withCode(new QCodeReference(ReceiveWebhookProcessStep.class)))); - - /////////////////////////////////////////// - // turn off audits (why on by default??) // - /////////////////////////////////////////// - //? qInstance.getTables().values().forEach(t -> t.setAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))); - - ///////////////// - // create apps // - ///////////////// - qInstance.addApp(new QAppMetaData() - .withName("receiptsApp") - .withLabel("Receipts") - .withIcon(new QIcon("receipt_long")) - .withSection(new QAppSection() - .withName("data") - .withTables(List.of(RECEIPT_TABLE_NAME)))); - - QJavalinMetaData.ofOrWithNew(qInstance) - .withRouteProvider(new JavalinRouteProviderMetaData() - .withName("defaultReceiver") - .withProcessName(RECEIVE_PROCESS_NAME) - .withHostedPath("/" + RECEIVE_PROCESS_NAME) - .withMethods(List.of("POST", "GET"))); - - return qInstance; - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static class ReceiveWebhookProcessStep implements BackendStep - { - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException - { - ProcessBasedRouterPayload processPayload = runBackendStepInput.getProcessPayload(ProcessBasedRouterPayload.class); - String content = processPayload.getBodyString(); - - QRecord insertedRecord = new InsertAction().execute(new InsertInput(RECEIPT_TABLE_NAME).withRecord(new QRecord().withValue("timestamp", Instant.now()).withValue("content", content))).getRecords().get(0); - System.out.println("I received and inserted: " + insertedRecord.getValues()); - - processPayload.setResponseString("OK"); - runBackendStepOutput.setProcessPayload(processPayload); - } - } - -} diff --git a/src/test/java/com/kingsrook/qbits/webhooks/WebhooksTestReceiverJavalinServer.java b/src/test/java/com/kingsrook/qbits/webhooks/WebhooksTestReceiverJavalinServer.java deleted file mode 100644 index 0880249..0000000 --- a/src/test/java/com/kingsrook/qbits/webhooks/WebhooksTestReceiverJavalinServer.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks; - - -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.middleware.javalin.QApplicationJavalinServer; - - -/******************************************************************************* - ** - *******************************************************************************/ -public class WebhooksTestReceiverJavalinServer -{ - - /*************************************************************************** - ** - ***************************************************************************/ - public static void main(String[] args) - { - startApplicationJavalinServer(); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - private static void startApplicationJavalinServer() - { - try - { - System.setProperty("qqq.javalin.hotSwapInstance", "false"); - Integer port = Integer.parseInt(System.getProperty("port", "7999")); - - QApplicationJavalinServer javalinServer = new QApplicationJavalinServer(new WebhooksTestReceiverApplication()) - .withPort(port) - .withServeFrontendMaterialDashboard(false); - javalinServer.start(); - } - catch(QException e) - { - e.printStackTrace(); - } - } - -} diff --git a/src/test/java/com/kingsrook/qbits/webhooks/actions/FireAdHocWebhookEventTest.java b/src/test/java/com/kingsrook/qbits/webhooks/actions/FireAdHocWebhookEventTest.java deleted file mode 100644 index bd86048..0000000 --- a/src/test/java/com/kingsrook/qbits/webhooks/actions/FireAdHocWebhookEventTest.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.actions; - - -import java.util.List; -import com.kingsrook.qbits.webhooks.BaseTest; -import com.kingsrook.qbits.webhooks.WebhooksTestApplication; -import com.kingsrook.qbits.webhooks.model.WebhookEvent; -import com.kingsrook.qbits.webhooks.model.WebhookEventCategory; -import com.kingsrook.qbits.webhooks.registry.WebhookEventType; -import com.kingsrook.qbits.webhooks.registry.WebhooksRegistry; -import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; -import com.kingsrook.qqq.backend.core.context.QContext; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.logging.QCollectingLogger; -import com.kingsrook.qqq.backend.core.logging.QLogger; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import org.json.JSONObject; -import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - - -/******************************************************************************* - ** Unit test for FireAdHocWebhookEvent - *******************************************************************************/ -class FireAdHocWebhookEventTest extends BaseTest -{ - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void test() throws QException - { - QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(FirePostInsertOrUpdateWebhookEventUtil.class); - - WebhooksRegistry webhooksRegistry = WebhooksRegistry.of(QContext.getQInstance()); - - String typeA = "typeA"; - webhooksRegistry.registerWebhookEventType(new WebhookEventType() - .withName(typeA) - .withCategory(WebhookEventCategory.AD_HOC)); - - String typeB = "typeB"; - webhooksRegistry.registerWebhookEventType(new WebhookEventType() - .withName(typeB) - .withCategory(WebhookEventCategory.AD_HOC)); - - //////////////////////////////////////////////// - // fire without any webhook - assert no log // - //////////////////////////////////////////////// - FireAdHocWebhookEvent fireAdHocWebhookEvent = new FireAdHocWebhookEvent(); - fireAdHocWebhookEvent.fire(typeA, WebhooksTestApplication.TABLE_NAME_PERSON, new QRecord().withValue("id", 1).withValue("firstName", "James T."), null); - assertThat(collectingLogger.getCollectedMessages()) - .noneMatch(m -> m.getMessage().matches(".*Building webhook event.*")); - - ///////////////////////////////////////////////////// - // make a sub - then fire, and assert positive log // - ///////////////////////////////////////////////////// - insert(newWebhookSubscription(typeA)); - WebhookSubscriptionsHelper.clearMemoizations(); - - fireAdHocWebhookEvent.fire(typeA, WebhooksTestApplication.TABLE_NAME_PERSON, new QRecord().withValue("id", 2).withValue("firstName", "Jean Luc"), null); - assertThat(collectingLogger.getCollectedMessages()) - .anyMatch(m -> m.getMessage().matches(".*Building webhook event.*")); - collectingLogger.clear(); - - /////////////////////////////////////////////// - // query to make sure event was inserted too // - /////////////////////////////////////////////// - List insertedEvents = new QueryAction().execute(new QueryInput(WebhookEvent.TABLE_NAME).withIncludeAssociations(true)).getRecords(); - assertEquals(1, insertedEvents.size()); - assertEquals(2, insertedEvents.get(0).getValueInteger("eventSourceRecordId")); - - //////////////////////////////////// - // assert about the contents too! // - //////////////////////////////////// - List contents = insertedEvents.get(0).getAssociatedRecords().get(WebhookEvent.CONTENT_ASSOCIATION_NAME); - assertEquals(1, contents.size()); - String postBody = contents.get(0).getValueString("postBody"); - assertNotNull(postBody); - JSONObject postBodyJson = new JSONObject(postBody); - assertTrue(postBodyJson.has("record")); - assertEquals("Jean Luc", postBodyJson.getJSONObject("record").getString("firstName")); - assertEquals(2, postBodyJson.getJSONObject("record").getInt("id")); - - ///////////////////////////////////////////////////////////////// - // fire for the other ad-hoc with no subs - and should NOT log // - ///////////////////////////////////////////////////////////////// - fireAdHocWebhookEvent.fire(typeB, WebhooksTestApplication.TABLE_NAME_PERSON, new QRecord().withValue("id", 1).withValue("firstName", "James T."), null); - assertThat(collectingLogger.getCollectedMessages()) - .noneMatch(m -> m.getMessage().matches(".*Building webhook event.*")); - } - -} \ No newline at end of file diff --git a/src/test/java/com/kingsrook/qbits/webhooks/actions/FirePostInsertWebhookEventTest.java b/src/test/java/com/kingsrook/qbits/webhooks/actions/FirePostInsertWebhookEventTest.java deleted file mode 100644 index 25c5125..0000000 --- a/src/test/java/com/kingsrook/qbits/webhooks/actions/FirePostInsertWebhookEventTest.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.actions; - - -import java.util.ArrayList; -import java.util.List; -import com.kingsrook.qbits.webhooks.BaseTest; -import com.kingsrook.qbits.webhooks.WebhooksTestApplication; -import com.kingsrook.qbits.webhooks.model.WebhookEvent; -import com.kingsrook.qbits.webhooks.model.WebhookEventCategory; -import com.kingsrook.qbits.webhooks.model.WebhooksActionFlags; -import com.kingsrook.qbits.webhooks.registry.WebhookEventType; -import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; -import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; -import com.kingsrook.qqq.backend.core.context.QContext; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.logging.QCollectingLogger; -import com.kingsrook.qqq.backend.core.logging.QLogger; -import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; -import org.json.JSONObject; -import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - - -/******************************************************************************* - ** Unit test for FirePostInsertWebhookEvent - *******************************************************************************/ -class FirePostInsertWebhookEventTest extends BaseTest -{ - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void test() throws QException - { - try - { - QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(FirePostInsertOrUpdateWebhookEventUtil.class); - - //////////////////////////////////////////////// - // insert without any webhook - assert no log // - //////////////////////////////////////////////// - new InsertAction().execute(new InsertInput(WebhooksTestApplication.TABLE_NAME_PERSON).withRecord(new QRecord())); - assertThat(collectingLogger.getCollectedMessages()) - .noneMatch(m -> m.getMessage().matches(".*Building webhook event.*")); - - /////////////////////////////////////////////////////////////////// - // register event type and make a sub - then assert positive log // - /////////////////////////////////////////////////////////////////// - insert(newWebhookSubscription(WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME)); - WebhookSubscriptionsHelper.clearMemoizations(); - - Integer personId = new InsertAction().execute(new InsertInput(WebhooksTestApplication.TABLE_NAME_PERSON) - .withRecord(new QRecord().withValue("firstName", "Bubba").withValue("lastName", "Fit"))) - .getRecords().get(0).getValueInteger("id"); - assertThat(collectingLogger.getCollectedMessages()) - .anyMatch(m -> m.getMessage().matches(".*Building webhook event.*")); - collectingLogger.clear(); - - /////////////////////////////////////////////// - // query to make sure event was inserted too // - /////////////////////////////////////////////// - List insertedEvents = new QueryAction().execute(new QueryInput(WebhookEvent.TABLE_NAME).withIncludeAssociations(true)).getRecords(); - assertEquals(1, insertedEvents.size()); - assertEquals(personId, insertedEvents.get(0).getValueInteger("eventSourceRecordId")); - - /////////////////////////////////////// - // run again, with omit flag present // - /////////////////////////////////////// - new InsertAction().execute(new InsertInput(WebhooksTestApplication.TABLE_NAME_PERSON) - .withFlag(WebhooksActionFlags.OMIT_WEBHOOKS) - .withRecord(new QRecord().withValue("firstName", "Bubba").withValue("lastName", "Fit"))) - .getRecords().get(0).getValueInteger("id"); - assertThat(collectingLogger.getCollectedMessages()) - .noneMatch(m -> m.getMessage().matches(".*Building webhook event.*")); - - //////////////////////////////////// - // assert about the contents too! // - //////////////////////////////////// - List contents = insertedEvents.get(0).getAssociatedRecords().get(WebhookEvent.CONTENT_ASSOCIATION_NAME); - assertEquals(1, contents.size()); - String postBody = contents.get(0).getValueString("postBody"); - assertNotNull(postBody); - JSONObject postBodyJson = new JSONObject(postBody); - assertTrue(postBodyJson.has("record")); - assertEquals("Bubba", postBodyJson.getJSONObject("record").getString("firstName")); - assertEquals(personId, postBodyJson.getJSONObject("record").getInt("id")); - } - finally - { - QLogger.deactivateCollectingLoggerForClass(FirePostInsertOrUpdateWebhookEventUtil.class); - } - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testStoreEventKind() throws QException - { - try - { - QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(FirePostInsertOrUpdateWebhookEventUtil.class); - - ///////////////////////////////////////////////////////////////////////////// - // register a 'store' event type and make a sub - then assert positive log // - ///////////////////////////////////////////////////////////////////////////// - insert(newWebhookSubscription(WebhooksTestApplication.PERSON_STORED_EVENT_TYPE_NAME)); - - Integer personId = new InsertAction().execute(new InsertInput(WebhooksTestApplication.TABLE_NAME_PERSON) - .withRecord(new QRecord().withValue("firstName", "Bubba").withValue("lastName", "Fit"))) - .getRecords().get(0).getValueInteger("id"); - assertThat(collectingLogger.getCollectedMessages()) - .anyMatch(m -> m.getMessage().matches(".*Building webhook event.*")); - collectingLogger.clear(); - - /////////////////////////////////////////////// - // query to make sure event was inserted too // - /////////////////////////////////////////////// - List insertedEvents = new QueryAction().execute(new QueryInput(WebhookEvent.TABLE_NAME).withIncludeAssociations(true)).getRecords(); - assertEquals(1, insertedEvents.size()); - assertEquals(personId, insertedEvents.get(0).getValueInteger("eventSourceRecordId")); - } - finally - { - QLogger.deactivateCollectingLoggerForClass(FirePostInsertOrUpdateWebhookEventUtil.class); - } - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testDoesRecordMatchWebhookEventType() - { - WebhookEventType insert = new WebhookEventType().withCategory(WebhookEventCategory.INSERT); - WebhookEventType insertFirstName = new WebhookEventType().withCategory(WebhookEventCategory.INSERT_WITH_FIELD).withFieldName("firstName"); - WebhookEventType insertFirstNameJohn = new WebhookEventType().withCategory(WebhookEventCategory.INSERT_WITH_VALUE).withFieldName("firstName").withValue("John"); - WebhookEventType insertFirstNameAnyBeatle = new WebhookEventType().withCategory(WebhookEventCategory.INSERT_WITH_VALUE).withFieldName("firstName").withValue(new ArrayList<>(List.of("John", "George", "Paul", "Ringo"))); - WebhookEventType store = new WebhookEventType().withCategory(WebhookEventCategory.STORE); - WebhookEventType storeFirstName = new WebhookEventType().withCategory(WebhookEventCategory.STORE_WITH_FIELD).withFieldName("firstName"); - WebhookEventType storeFirstNameJohn = new WebhookEventType().withCategory(WebhookEventCategory.STORE_WITH_VALUE).withFieldName("firstName").withValue("John"); - - QFieldMetaData field = QContext.getQInstance().getTable(WebhooksTestApplication.TABLE_NAME_PERSON).getField("firstName"); - - QRecord emptyRecord = new QRecord(); - assertTrue(doesMatch(emptyRecord, insert, field)); - assertFalse(doesMatch(emptyRecord, insertFirstName, field)); - assertFalse(doesMatch(emptyRecord, insertFirstNameJohn, field)); - assertFalse(doesMatch(emptyRecord, insertFirstNameAnyBeatle, field)); - assertTrue(doesMatch(emptyRecord, store, field)); - assertFalse(doesMatch(emptyRecord, storeFirstName, field)); - assertFalse(doesMatch(emptyRecord, storeFirstNameJohn, field)); - - QRecord firstNameJoeRecord = new QRecord().withValue("firstName", "Joe"); - assertTrue(doesMatch(firstNameJoeRecord, insert, field)); - assertTrue(doesMatch(firstNameJoeRecord, insertFirstName, field)); - assertFalse(doesMatch(firstNameJoeRecord, insertFirstNameJohn, field)); - assertFalse(doesMatch(firstNameJoeRecord, insertFirstNameAnyBeatle, field)); - assertTrue(doesMatch(firstNameJoeRecord, store, field)); - assertTrue(doesMatch(firstNameJoeRecord, storeFirstName, field)); - assertFalse(doesMatch(firstNameJoeRecord, storeFirstNameJohn, field)); - - QRecord firstNameJohnRecord = new QRecord().withValue("firstName", "John"); - assertTrue(doesMatch(firstNameJohnRecord, insert, field)); - assertTrue(doesMatch(firstNameJohnRecord, insertFirstName, field)); - assertTrue(doesMatch(firstNameJohnRecord, insertFirstNameJohn, field)); - assertTrue(doesMatch(firstNameJohnRecord, insertFirstNameAnyBeatle, field)); - assertTrue(doesMatch(firstNameJohnRecord, store, field)); - assertTrue(doesMatch(firstNameJohnRecord, storeFirstName, field)); - assertTrue(doesMatch(firstNameJohnRecord, storeFirstNameJohn, field)); - - QRecord firstNameRingoRecord = new QRecord().withValue("firstName", "Ringo"); - assertTrue(doesMatch(firstNameRingoRecord, insert, field)); - assertTrue(doesMatch(firstNameRingoRecord, insertFirstName, field)); - assertFalse(doesMatch(firstNameRingoRecord, insertFirstNameJohn, field)); - assertTrue(doesMatch(firstNameRingoRecord, insertFirstNameAnyBeatle, field)); - assertTrue(doesMatch(firstNameRingoRecord, store, field)); - assertTrue(doesMatch(firstNameRingoRecord, storeFirstName, field)); - assertFalse(doesMatch(firstNameRingoRecord, storeFirstNameJohn, field)); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - private boolean doesMatch(QRecord record, WebhookEventType webhookEventType, QFieldMetaData field) - { - return new FirePostInsertWebhookEvent().doesRecordMatchWebhookEventType(record, webhookEventType, field); - } -} \ No newline at end of file diff --git a/src/test/java/com/kingsrook/qbits/webhooks/actions/FirePostUpdateWebhookEventTest.java b/src/test/java/com/kingsrook/qbits/webhooks/actions/FirePostUpdateWebhookEventTest.java deleted file mode 100644 index 9bf11d3..0000000 --- a/src/test/java/com/kingsrook/qbits/webhooks/actions/FirePostUpdateWebhookEventTest.java +++ /dev/null @@ -1,306 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.actions; - - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import com.kingsrook.qbits.webhooks.BaseTest; -import com.kingsrook.qbits.webhooks.WebhooksTestApplication; -import com.kingsrook.qbits.webhooks.model.Webhook; -import com.kingsrook.qbits.webhooks.model.WebhookEvent; -import com.kingsrook.qbits.webhooks.model.WebhookEventCategory; -import com.kingsrook.qbits.webhooks.model.WebhooksActionFlags; -import com.kingsrook.qbits.webhooks.registry.WebhookEventType; -import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; -import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; -import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; -import com.kingsrook.qqq.backend.core.context.QContext; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.logging.QCollectingLogger; -import com.kingsrook.qqq.backend.core.logging.QLogger; -import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; -import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; -import org.json.JSONObject; -import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - - -/******************************************************************************* - ** Unit test for FirePostInsertWebhookEvent - *******************************************************************************/ -class FirePostUpdateWebhookEventTest extends BaseTest -{ - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void test() throws QException - { - try - { - Integer personId = new InsertAction().execute(new InsertInput(WebhooksTestApplication.TABLE_NAME_PERSON).withRecord(new QRecord())).getRecords().get(0).getValueInteger("id"); - - QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(FirePostInsertOrUpdateWebhookEventUtil.class); - - //////////////////////////////////////////////// - // update without any webhook - assert no log // - //////////////////////////////////////////////// - new UpdateAction().execute(new UpdateInput(WebhooksTestApplication.TABLE_NAME_PERSON).withRecord(new QRecord().withValue("id", personId).withValue("firstName", "Bubba"))); - assertThat(collectingLogger.getCollectedMessages()) - .noneMatch(m -> m.getMessage().matches(".*Building webhook event.*")); - - /////////////////////////////////////////////////////////////////// - // register event type and make a sub - then assert positive log // - /////////////////////////////////////////////////////////////////// - String eventTypeName = "updatedPerson"; - registerEventType(eventTypeName, WebhookEventCategory.UPDATE, WebhooksTestApplication.TABLE_NAME_PERSON); - Integer subscriptionId = insert(newWebhookSubscription(eventTypeName)); - - new UpdateAction().execute(new UpdateInput(WebhooksTestApplication.TABLE_NAME_PERSON) - .withRecord(new QRecord().withValue("id", personId).withValue("firstName", "Bobby"))); - assertThat(collectingLogger.getCollectedMessages()) - .anyMatch(m -> m.getMessage().matches(".*Building webhook event.*")); - collectingLogger.clear(); - - /////////////////////////////////////////////// - // query to make sure event was inserted too // - /////////////////////////////////////////////// - List insertedEvents = new QueryAction().execute(new QueryInput(WebhookEvent.TABLE_NAME) - .withFilter(new QQueryFilter(new QFilterCriteria("webhookSubscriptionId", QCriteriaOperator.EQUALS, subscriptionId))) - .withIncludeAssociations(true)).getRecords(); - assertEquals(1, insertedEvents.size()); - assertEquals(personId, insertedEvents.get(0).getValueInteger("eventSourceRecordId")); - - /////////////////////////////////////// - // run again, with omit flag present // - /////////////////////////////////////// - new UpdateAction().execute(new UpdateInput(WebhooksTestApplication.TABLE_NAME_PERSON) - .withFlag(WebhooksActionFlags.OMIT_WEBHOOKS) - .withRecord(new QRecord().withValue("id", personId).withValue("firstName", "Bobby"))); - assertThat(collectingLogger.getCollectedMessages()) - .noneMatch(m -> m.getMessage().matches(".*Building webhook event.*")); - - //////////////////////////////////// - // assert about the contents too! // - //////////////////////////////////// - List contents = insertedEvents.get(0).getAssociatedRecords().get(WebhookEvent.CONTENT_ASSOCIATION_NAME); - assertEquals(1, contents.size()); - String postBody = contents.get(0).getValueString("postBody"); - assertNotNull(postBody); - JSONObject postBodyJson = new JSONObject(postBody); - assertTrue(postBodyJson.has("record")); - assertEquals("Bobby", postBodyJson.getJSONObject("record").getString("firstName")); - assertEquals(personId, postBodyJson.getJSONObject("record").getInt("id")); - } - finally - { - QLogger.deactivateCollectingLoggerForClass(FirePostInsertOrUpdateWebhookEventUtil.class); - } - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testStoreEventType() throws QException - { - try - { - Integer personId = new InsertAction().execute(new InsertInput(WebhooksTestApplication.TABLE_NAME_PERSON).withRecord(new QRecord())).getRecords().get(0).getValueInteger("id"); - - QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(FirePostInsertOrUpdateWebhookEventUtil.class); - - /////////////////////////////////////////////////////////////////// - // register event type and make a sub - then assert positive log // - /////////////////////////////////////////////////////////////////// - Integer subscriptionId = insert(newWebhookSubscription(WebhooksTestApplication.PERSON_STORED_EVENT_TYPE_NAME)); - - new UpdateAction().execute(new UpdateInput(WebhooksTestApplication.TABLE_NAME_PERSON) - .withRecord(new QRecord().withValue("id", personId).withValue("firstName", "Bobby"))); - assertThat(collectingLogger.getCollectedMessages()) - .anyMatch(m -> m.getMessage().matches(".*Building webhook event.*")); - collectingLogger.clear(); - - /////////////////////////////////////////////// - // query to make sure event was inserted too // - /////////////////////////////////////////////// - List insertedEvents = new QueryAction().execute(new QueryInput(WebhookEvent.TABLE_NAME) - .withFilter(new QQueryFilter(new QFilterCriteria("webhookSubscriptionId", QCriteriaOperator.EQUALS, subscriptionId))) - .withIncludeAssociations(true)).getRecords(); - assertEquals(1, insertedEvents.size()); - assertEquals(personId, insertedEvents.get(0).getValueInteger("eventSourceRecordId")); - } - finally - { - QLogger.deactivateCollectingLoggerForClass(FirePostInsertOrUpdateWebhookEventUtil.class); - } - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testSecurityKeys() throws QException - { - Integer storeA = 17; - Integer storeB = 18; - Integer orderIdA = new InsertAction().execute(new InsertInput(WebhooksTestApplication.TABLE_NAME_ORDER).withRecord(new QRecord().withValue("storeId", storeA))).getRecords().get(0).getValueInteger("id"); - Integer orderIdB = new InsertAction().execute(new InsertInput(WebhooksTestApplication.TABLE_NAME_ORDER).withRecord(new QRecord().withValue("storeId", storeB))).getRecords().get(0).getValueInteger("id"); - - ///////////////////////////////////////////////////////////////////// - // register event type and make a webhook with a sub - for store A // - ///////////////////////////////////////////////////////////////////// - String eventTypeName = "updatedOrder"; - registerEventType(eventTypeName, WebhookEventCategory.UPDATE, WebhooksTestApplication.TABLE_NAME_ORDER); - Integer webhookId = new InsertAction().execute(new InsertInput(Webhook.TABLE_NAME).withRecord(newWebhook(eventTypeName).toQRecord().withValue("storeId", storeA))).getRecords().get(0).getValueInteger("id"); - Integer subscriptionId = insert(newWebhookSubscription(eventTypeName).withWebhookId(webhookId)); - - /////////////////////////////////////////////////////////////////////////////////// - // update the 2 orders - only an event for A should fire // - // of note - the record that's updated doesn't have its storeId field in it - // - // so originally this case failed to match the subscription - but, now it works. // - /////////////////////////////////////////////////////////////////////////////////// - new UpdateAction().execute(new UpdateInput(WebhooksTestApplication.TABLE_NAME_ORDER).withRecord(new QRecord().withValue("id", orderIdA).withValue("orderNo", "A"))); - new UpdateAction().execute(new UpdateInput(WebhooksTestApplication.TABLE_NAME_ORDER).withRecord(new QRecord().withValue("id", orderIdB).withValue("orderNo", "B"))); - - List insertedEvents = new QueryAction().execute(new QueryInput(WebhookEvent.TABLE_NAME) - .withFilter(new QQueryFilter(new QFilterCriteria("webhookSubscriptionId", QCriteriaOperator.EQUALS, subscriptionId))) - .withIncludeAssociations(true)).getRecords(); - assertEquals(1, insertedEvents.size()); - assertEquals(orderIdA, insertedEvents.get(0).getValueInteger("eventSourceRecordId")); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testDoesRecordMatchWebhookEventType() - { - WebhookEventType update = new WebhookEventType().withTableName(WebhooksTestApplication.TABLE_NAME_PERSON).withCategory(WebhookEventCategory.UPDATE); - WebhookEventType updateFirstName = new WebhookEventType().withTableName(WebhooksTestApplication.TABLE_NAME_PERSON).withCategory(WebhookEventCategory.UPDATE_WITH_FIELD).withFieldName("firstName"); - WebhookEventType updateFirstNameJohn = new WebhookEventType().withTableName(WebhooksTestApplication.TABLE_NAME_PERSON).withCategory(WebhookEventCategory.UPDATE_WITH_VALUE).withFieldName("firstName").withValue("John"); - WebhookEventType updateFirstNameAnyBeatle = new WebhookEventType().withTableName(WebhooksTestApplication.TABLE_NAME_PERSON).withCategory(WebhookEventCategory.UPDATE_WITH_VALUE).withFieldName("firstName").withValue(new ArrayList<>(List.of("John", "George", "Paul", "Ringo"))); - - //////////////////////////////////////////////// - // w/ no values, only the generic rule passes // - //////////////////////////////////////////////// - QRecord blankRecord = new QRecord(); - assertTrue(doesMatch(blankRecord, blankRecord, update)); - assertFalse(doesMatch(blankRecord, blankRecord, updateFirstName)); - assertFalse(doesMatch(blankRecord, blankRecord, updateFirstNameJohn)); - assertFalse(doesMatch(blankRecord, blankRecord, updateFirstNameAnyBeatle)); - - ///////////////////////////////////////////////////////////////////////// - // w/ firstname set to Joe, the generic and "any firstname" rules pass // - ///////////////////////////////////////////////////////////////////////// - QRecord firstNameJoeRecord = new QRecord().withValue("firstName", "Joe"); - assertTrue(doesMatch(firstNameJoeRecord, blankRecord, update)); - assertTrue(doesMatch(firstNameJoeRecord, blankRecord, updateFirstName)); - assertFalse(doesMatch(firstNameJoeRecord, blankRecord, updateFirstNameJohn)); - assertFalse(doesMatch(firstNameJoeRecord, blankRecord, updateFirstNameAnyBeatle)); - - ///////////////////////////////////////////////////////////////////////////////// - // an update that doesn't change firstName won't fire the "any firstname" rule // - ///////////////////////////////////////////////////////////////////////////////// - QRecord firstNameJoeLastNameSmithRecord = new QRecord().withValue("firstName", "Joe").withValue("lastName", "Smith"); - QRecord firstNameJoeLastNameJonesRecord = new QRecord().withValue("firstName", "Joe").withValue("lastName", "Jones"); - assertTrue(doesMatch(firstNameJoeLastNameSmithRecord, firstNameJoeLastNameJonesRecord, update)); - assertFalse(doesMatch(firstNameJoeLastNameSmithRecord, firstNameJoeLastNameJonesRecord, updateFirstName)); - assertFalse(doesMatch(firstNameJoeLastNameSmithRecord, firstNameJoeLastNameJonesRecord, updateFirstNameJohn)); - assertFalse(doesMatch(firstNameJoeLastNameSmithRecord, firstNameJoeLastNameJonesRecord, updateFirstNameAnyBeatle)); - - /////////////////////////////////////////////////////////////////////// - // an update that only changes last name only fires the generic rule // - /////////////////////////////////////////////////////////////////////// - QRecord lastNameJonesRecord = new QRecord().withValue("lastName", "Jones"); - assertTrue(doesMatch(lastNameJonesRecord, blankRecord, update)); - assertFalse(doesMatch(lastNameJonesRecord, blankRecord, updateFirstName)); - assertFalse(doesMatch(lastNameJonesRecord, blankRecord, updateFirstNameJohn)); - assertFalse(doesMatch(lastNameJonesRecord, blankRecord, updateFirstNameAnyBeatle)); - - //////////////////////////////////////////////////////////////// - // changing first name to the target value passes all 4 rules // - //////////////////////////////////////////////////////////////// - QRecord firstNameJohnRecord = new QRecord().withValue("firstName", "John"); - assertTrue(doesMatch(firstNameJohnRecord, blankRecord, update)); - assertTrue(doesMatch(firstNameJohnRecord, blankRecord, updateFirstName)); - assertTrue(doesMatch(firstNameJohnRecord, blankRecord, updateFirstNameJohn)); - assertTrue(doesMatch(firstNameJohnRecord, blankRecord, updateFirstNameAnyBeatle)); - - ///////////////////////////////////////////////////////////////////////////////////////// - // changing first name to the target value from the list but not the single-value rule // - ///////////////////////////////////////////////////////////////////////////////////////// - QRecord firstNameRingoRecord = new QRecord().withValue("firstName", "Ringo"); - assertTrue(doesMatch(firstNameRingoRecord, blankRecord, update)); - assertTrue(doesMatch(firstNameRingoRecord, blankRecord, updateFirstName)); - assertFalse(doesMatch(firstNameRingoRecord, blankRecord, updateFirstNameJohn)); - assertTrue(doesMatch(firstNameRingoRecord, blankRecord, updateFirstNameAnyBeatle)); - - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // an update that doesn't change firstName (even if it has the target value) won't fire the "firstname John" rule // - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - QRecord firstNameJohnRecordLastNameSmithRecord = new QRecord().withValue("firstName", "John").withValue("lastName", "Smith"); - QRecord firstNameJohnRecordLastNameJonesRecord = new QRecord().withValue("firstName", "John").withValue("lastName", "Jones"); - assertTrue(doesMatch(firstNameJohnRecordLastNameSmithRecord, firstNameJohnRecordLastNameJonesRecord, update)); - assertFalse(doesMatch(firstNameJohnRecordLastNameSmithRecord, firstNameJohnRecordLastNameJonesRecord, updateFirstName)); - assertFalse(doesMatch(firstNameJohnRecordLastNameSmithRecord, firstNameJohnRecordLastNameJonesRecord, updateFirstNameJohn)); - assertFalse(doesMatch(firstNameJohnRecordLastNameSmithRecord, firstNameJohnRecordLastNameJonesRecord, updateFirstNameAnyBeatle)); - - ////////////////////////////////////////////////////////////// - // without old-record, always fail the value-checking rules // - ////////////////////////////////////////////////////////////// - assertTrue(doesMatch(firstNameJohnRecord, null, update)); - assertFalse(doesMatch(firstNameJohnRecord, null, updateFirstName)); - assertFalse(doesMatch(firstNameJohnRecord, null, updateFirstNameJohn)); - assertFalse(doesMatch(firstNameJohnRecord, null, updateFirstNameAnyBeatle)); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - private boolean doesMatch(QRecord record, QRecord oldRecord, WebhookEventType webhookEventType) - { - FirePostUpdateWebhookEvent firePostUpdateWebhookEvent = new FirePostUpdateWebhookEvent(); - QFieldMetaData field = QContext.getQInstance().getTable(webhookEventType.getTableName()).getFields().get(webhookEventType.getFieldName()); - return firePostUpdateWebhookEvent.doesRecordMatchWebhookEventType(record, Optional.ofNullable(oldRecord), webhookEventType, field); - } -} \ No newline at end of file diff --git a/src/test/java/com/kingsrook/qbits/webhooks/actions/WebhookEventSenderTest.java b/src/test/java/com/kingsrook/qbits/webhooks/actions/WebhookEventSenderTest.java deleted file mode 100644 index 672081d..0000000 --- a/src/test/java/com/kingsrook/qbits/webhooks/actions/WebhookEventSenderTest.java +++ /dev/null @@ -1,880 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.actions; - - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Random; -import com.kingsrook.qbits.webhooks.BaseTest; -import com.kingsrook.qbits.webhooks.WebhooksTestApplication; -import com.kingsrook.qbits.webhooks.model.Webhook; -import com.kingsrook.qbits.webhooks.model.WebhookEvent; -import com.kingsrook.qbits.webhooks.model.WebhookEventContent; -import com.kingsrook.qbits.webhooks.model.WebhookEventSendLog; -import com.kingsrook.qbits.webhooks.model.WebhookEventStatus; -import com.kingsrook.qbits.webhooks.model.WebhookHealthStatus; -import com.kingsrook.qbits.webhooks.model.WebhookSubscription; -import com.kingsrook.qqq.backend.core.actions.tables.GetAction; -import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.logging.QCollectingLogger; -import com.kingsrook.qqq.backend.core.logging.QLogger; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; -import org.apache.http.Header; -import org.apache.http.HeaderIterator; -import org.apache.http.HttpEntity; -import org.apache.http.HttpHost; -import org.apache.http.HttpRequest; -import org.apache.http.ProtocolVersion; -import org.apache.http.StatusLine; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.conn.ClientConnectionManager; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.message.BasicStatusLine; -import org.apache.http.params.HttpParams; -import org.apache.http.protocol.HttpContext; -import org.assertj.core.data.TemporalUnitWithinOffset; -import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - - -/******************************************************************************* - ** Unit test for WebhookEventSender - *******************************************************************************/ -public class WebhookEventSenderTest extends BaseTest -{ - private TemporalUnitWithinOffset WITHIN_5_SEC = new TemporalUnitWithinOffset(5, ChronoUnit.SECONDS); - - QQueryFilter sendLogFilter = new QQueryFilter().withOrderBy(new QFilterOrderBy("id", false)); - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testSimpleSuccess() throws QException - { - ///////////////////////////////////////////////////////////////// - // use random ids to avoid everything being 1 and false-passes // - ///////////////////////////////////////////////////////////////// - Integer webhookId = insert(newWebhook("Test")); - Integer subscriptionId = insert(newWebhookSubscription(WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME).withWebhookId(webhookId)); - Integer eventId = insert(newWebhookEvent(new WebhookSubscription(GetAction.execute(WebhookSubscription.TABLE_NAME, subscriptionId)), WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME)); - - WebhookEvent event = new WebhookEvent(GetAction.execute(WebhookEvent.TABLE_NAME, eventId)); - Webhook webhook = new Webhook(GetAction.execute(Webhook.TABLE_NAME, webhookId)); - List sendLogs = new ArrayList<>(); - - ///////////////// - // try to send // - ///////////////// - WebhookEventSender sender = new WebhookEventSenderThatSucceeds(); - boolean wasSuccessful = sender.handleEvent(event, webhook, sendLogs); - - /////////////////////////////// - // assert failure everywhere // - /////////////////////////////// - assertTrue(wasSuccessful); - - List insertedSendLogs = QueryAction.execute(WebhookEventSendLog.TABLE_NAME, sendLogFilter); - assertEquals(1, insertedSendLogs.size()); - - WebhookEventSendLog sendLog = new WebhookEventSendLog(insertedSendLogs.get(0)); - assertNull(sendLog.getErrorMessage()); - assertEquals(1, sendLog.getAttemptNo()); - assertEquals(webhookId, sendLog.getWebhookId()); - assertEquals(eventId, sendLog.getWebhookEventId()); - assertTrue(sendLog.getSuccessful()); - assertThat(sendLog.getStartTimestamp()).isCloseTo(Instant.now(), WITHIN_5_SEC); - assertThat(sendLog.getEndTimestamp()).isCloseTo(Instant.now(), WITHIN_5_SEC); - - WebhookEvent updatedEvent = new WebhookEvent(GetAction.execute(WebhookEvent.TABLE_NAME, eventId)); - assertEquals(WebhookEventStatus.DELIVERED.getId(), updatedEvent.getEventStatusId()); - assertNull(updatedEvent.getNextAttemptTimestamp()); - sendLogs.add(sendLog); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testFiveFailures() throws QException - { - ///////////////////////////////////////////////////////////////// - // use random ids to avoid everything being 1 and false-passes // - ///////////////////////////////////////////////////////////////// - Random random = new Random(); - Integer webhookId = insert(newWebhook("Test")); - Integer subscriptionId = insert(newWebhookSubscription(WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME).withWebhookId(webhookId)); - Integer eventId = insert(newWebhookEvent(new WebhookSubscription(GetAction.execute(WebhookSubscription.TABLE_NAME, subscriptionId)), WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME)); - - WebhookEvent event = new WebhookEvent(GetAction.execute(WebhookEvent.TABLE_NAME, eventId)); - Webhook webhook = new Webhook(GetAction.execute(Webhook.TABLE_NAME, webhookId)); - List sendLogs = new ArrayList<>(); - - ///////////////// - // try to send // - ///////////////// - WebhookEventSender sender = new WebhookEventSenderThatFails(); - boolean wasSuccessful = sender.handleEvent(event, webhook, sendLogs); - - /////////////////////////////// - // assert failure everywhere // - /////////////////////////////// - assertFalse(wasSuccessful); - - List insertedSendLogs = QueryAction.execute(WebhookEventSendLog.TABLE_NAME, sendLogFilter); - assertEquals(1, insertedSendLogs.size()); - - WebhookEventSendLog sendLog = new WebhookEventSendLog(insertedSendLogs.get(0)); - assertEquals("Test failure", sendLog.getErrorMessage()); - assertEquals(1, sendLog.getAttemptNo()); - assertEquals(webhookId, sendLog.getWebhookId()); - assertEquals(eventId, sendLog.getWebhookEventId()); - assertFalse(sendLog.getSuccessful()); - assertThat(sendLog.getStartTimestamp()).isCloseTo(Instant.now(), WITHIN_5_SEC); - assertThat(sendLog.getEndTimestamp()).isCloseTo(Instant.now(), WITHIN_5_SEC); - - WebhookEvent updatedEvent = new WebhookEvent(GetAction.execute(WebhookEvent.TABLE_NAME, eventId)); - assertEquals(WebhookEventStatus.AWAITING_RETRY.getId(), updatedEvent.getEventStatusId()); - assertThat(updatedEvent.getNextAttemptTimestamp()).isCloseTo(Instant.now().plusSeconds(60), WITHIN_5_SEC); - sendLogs.add(sendLog); - - //////////// - // try #2 // - //////////// - wasSuccessful = sender.handleEvent(updatedEvent, webhook, sendLogs); - assertFalse(wasSuccessful); - - insertedSendLogs = QueryAction.execute(WebhookEventSendLog.TABLE_NAME, sendLogFilter); - assertEquals(2, insertedSendLogs.size()); - sendLog = new WebhookEventSendLog(insertedSendLogs.get(0)); - assertEquals(2, sendLog.getAttemptNo()); - - updatedEvent = new WebhookEvent(GetAction.execute(WebhookEvent.TABLE_NAME, eventId)); - assertEquals(WebhookEventStatus.AWAITING_RETRY.getId(), updatedEvent.getEventStatusId()); - assertThat(updatedEvent.getNextAttemptTimestamp()).isCloseTo(Instant.now().plusSeconds(5 * 60), WITHIN_5_SEC); - sendLogs.add(sendLog); - - //////////// - // try #3 // - //////////// - wasSuccessful = sender.handleEvent(updatedEvent, webhook, sendLogs); - assertFalse(wasSuccessful); - - insertedSendLogs = QueryAction.execute(WebhookEventSendLog.TABLE_NAME, sendLogFilter); - assertEquals(3, insertedSendLogs.size()); - sendLog = new WebhookEventSendLog(insertedSendLogs.get(0)); - assertEquals(3, sendLog.getAttemptNo()); - - updatedEvent = new WebhookEvent(GetAction.execute(WebhookEvent.TABLE_NAME, eventId)); - assertEquals(WebhookEventStatus.AWAITING_RETRY.getId(), updatedEvent.getEventStatusId()); - assertThat(updatedEvent.getNextAttemptTimestamp()).isCloseTo(Instant.now().plusSeconds(15 * 60), WITHIN_5_SEC); - sendLogs.add(sendLog); - - //////////// - // try #4 // - //////////// - wasSuccessful = sender.handleEvent(updatedEvent, webhook, sendLogs); - assertFalse(wasSuccessful); - - insertedSendLogs = QueryAction.execute(WebhookEventSendLog.TABLE_NAME, sendLogFilter); - assertEquals(4, insertedSendLogs.size()); - sendLog = new WebhookEventSendLog(insertedSendLogs.get(0)); - assertEquals(4, sendLog.getAttemptNo()); - - updatedEvent = new WebhookEvent(GetAction.execute(WebhookEvent.TABLE_NAME, eventId)); - assertEquals(WebhookEventStatus.AWAITING_RETRY.getId(), updatedEvent.getEventStatusId()); - assertThat(updatedEvent.getNextAttemptTimestamp()).isCloseTo(Instant.now().plusSeconds(60 * 60), WITHIN_5_SEC); - sendLogs.add(sendLog); - - //////////// - // try #5 // - //////////// - wasSuccessful = sender.handleEvent(updatedEvent, webhook, sendLogs); - assertFalse(wasSuccessful); - - insertedSendLogs = QueryAction.execute(WebhookEventSendLog.TABLE_NAME, sendLogFilter); - assertEquals(5, insertedSendLogs.size()); - sendLog = new WebhookEventSendLog(insertedSendLogs.get(0)); - assertEquals(5, sendLog.getAttemptNo()); - - updatedEvent = new WebhookEvent(GetAction.execute(WebhookEvent.TABLE_NAME, eventId)); - assertEquals(WebhookEventStatus.FAILED.getId(), updatedEvent.getEventStatusId()); - assertNull(updatedEvent.getNextAttemptTimestamp()); - sendLogs.add(sendLog); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testFifthFailureOnProbation() throws QException - { - ///////////////////////////////////////////////////////////////// - // use random ids to avoid everything being 1 and false-passes // - ///////////////////////////////////////////////////////////////// - Random random = new Random(); - Integer webhookId = insert(newWebhook("Test").withHealthStatusId(WebhookHealthStatus.PROBATION.getId())); - Integer subscriptionId = insert(newWebhookSubscription(WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME).withWebhookId(webhookId)); - Integer eventId = insert(newWebhookEvent(new WebhookSubscription(GetAction.execute(WebhookSubscription.TABLE_NAME, subscriptionId)), WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME)); - - WebhookEvent event = new WebhookEvent(GetAction.execute(WebhookEvent.TABLE_NAME, eventId)); - Webhook webhook = new Webhook(GetAction.execute(Webhook.TABLE_NAME, webhookId)); - List sendLogs = ListBuilder.of( - new WebhookEventSendLog().withAttemptNo(1), - new WebhookEventSendLog().withAttemptNo(2), - new WebhookEventSendLog().withAttemptNo(3), - new WebhookEventSendLog().withAttemptNo(4) - ); - - ///////////////// - // try to send // - ///////////////// - WebhookEventSender sender = new WebhookEventSenderThatFails(); - boolean wasSuccessful = sender.handleEvent(event, webhook, sendLogs); - - //////////////////// - // assert failure // - //////////////////// - assertFalse(wasSuccessful); - - /////////////////////////////////////////////////////////////////////// - // make sure we didn't go into failure, but stayed in awaiting retry // - /////////////////////////////////////////////////////////////////////// - WebhookEvent updatedEvent = new WebhookEvent(GetAction.execute(WebhookEvent.TABLE_NAME, eventId)); - assertEquals(WebhookEventStatus.AWAITING_RETRY.getId(), updatedEvent.getEventStatusId()); - assertThat(updatedEvent.getNextAttemptTimestamp()).isCloseTo(Instant.now().plusSeconds(4 * 60 * 60), WITHIN_5_SEC); - - List insertedSendLogs = QueryAction.execute(WebhookEventSendLog.TABLE_NAME, sendLogFilter); - WebhookEventSendLog sendLog = new WebhookEventSendLog(insertedSendLogs.get(0)); - assertEquals(5, sendLog.getAttemptNo()); - sendLogs.add(sendLog); - - ///////////////////////////////////////////////////// - // try once more - attemptNo should go up beyond 5 // - ///////////////////////////////////////////////////// - sender.handleEvent(event, webhook, sendLogs); - - updatedEvent = new WebhookEvent(GetAction.execute(WebhookEvent.TABLE_NAME, eventId)); - assertEquals(WebhookEventStatus.AWAITING_RETRY.getId(), updatedEvent.getEventStatusId()); - assertThat(updatedEvent.getNextAttemptTimestamp()).isCloseTo(Instant.now().plusSeconds(4 * 60 * 60), WITHIN_5_SEC); - - insertedSendLogs = QueryAction.execute(WebhookEventSendLog.TABLE_NAME, sendLogFilter); - sendLog = new WebhookEventSendLog(insertedSendLogs.get(0)); - assertEquals(6, sendLog.getAttemptNo()); - sendLogs.add(sendLog); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testHttpStatusCodes() throws QException - { - ///////////////////////////////////////////////////////////////// - // use random ids to avoid everything being 1 and false-passes // - ///////////////////////////////////////////////////////////////// - Random random = new Random(); - Integer webhookId = insert(newWebhook("Test")); - Integer subscriptionId = insert(newWebhookSubscription(WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME).withWebhookId(webhookId)); - Integer eventId = insert(newWebhookEvent(new WebhookSubscription(GetAction.execute(WebhookSubscription.TABLE_NAME, subscriptionId)), WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME)); - - WebhookEvent event = new WebhookEvent(GetAction.execute(WebhookEvent.TABLE_NAME, eventId)); - event.setContent(List.of(new WebhookEventContent().withWebhookEventId(eventId).withPostBody("{}"))); - - Webhook webhook = new Webhook(GetAction.execute(Webhook.TABLE_NAME, webhookId)); - List sendLogs = new ArrayList<>(); - - //////////////// - // 200 - okay // - //////////////// - { - WebhookEventSender sender = new WebhookEventSenderThatMocksHttp(200, "OK"); - assertTrue(sender.handleEvent(event, webhook, sendLogs)); - List insertedSendLogs = QueryAction.execute(WebhookEventSendLog.TABLE_NAME, sendLogFilter); - WebhookEventSendLog sendLog = new WebhookEventSendLog(insertedSendLogs.get(0)); - assertNull(sendLog.getErrorMessage()); - assertTrue(sendLog.getSuccessful()); - assertEquals(200, sendLog.getHttpStatusCode()); - } - - //////////////// - // 201 - okay // - //////////////// - { - WebhookEventSender sender = new WebhookEventSenderThatMocksHttp(201, "OK"); - assertTrue(sender.handleEvent(event, webhook, sendLogs)); - List insertedSendLogs = QueryAction.execute(WebhookEventSendLog.TABLE_NAME, sendLogFilter); - WebhookEventSendLog sendLog = new WebhookEventSendLog(insertedSendLogs.get(0)); - assertNull(sendLog.getErrorMessage()); - assertTrue(sendLog.getSuccessful()); - assertEquals(201, sendLog.getHttpStatusCode()); - } - - //////////////// - // 301 - fail // - //////////////// - { - WebhookEventSender sender = new WebhookEventSenderThatMocksHttp(301, "Go away"); - assertFalse(sender.handleEvent(event, webhook, sendLogs)); - List insertedSendLogs = QueryAction.execute(WebhookEventSendLog.TABLE_NAME, sendLogFilter); - WebhookEventSendLog sendLog = new WebhookEventSendLog(insertedSendLogs.get(0)); - assertEquals("Go away", sendLog.getErrorMessage()); - assertFalse(sendLog.getSuccessful()); - assertEquals(301, sendLog.getHttpStatusCode()); - } - - ///////////////////////////// - // 500 with no body - fail // - ///////////////////////////// - { - WebhookEventSender sender = new WebhookEventSenderThatMocksHttp(500, ""); - assertFalse(sender.handleEvent(event, webhook, sendLogs)); - List insertedSendLogs = QueryAction.execute(WebhookEventSendLog.TABLE_NAME, sendLogFilter); - WebhookEventSendLog sendLog = new WebhookEventSendLog(insertedSendLogs.get(0)); - assertEquals("No response body returned", sendLog.getErrorMessage()); - assertFalse(sendLog.getSuccessful()); - assertEquals(500, sendLog.getHttpStatusCode()); - } - - //////////////// - // 199 - fail // - //////////////// - { - WebhookEventSender sender = new WebhookEventSenderThatMocksHttp(199, "what even?"); - assertFalse(sender.handleEvent(event, webhook, sendLogs)); - List insertedSendLogs = QueryAction.execute(WebhookEventSendLog.TABLE_NAME, sendLogFilter); - WebhookEventSendLog sendLog = new WebhookEventSendLog(insertedSendLogs.get(0)); - assertEquals("what even?", sendLog.getErrorMessage()); - assertFalse(sendLog.getSuccessful()); - assertEquals(199, sendLog.getHttpStatusCode()); - } - - //////////////////////////////////////// - // IO Exception (e.g., can't connect) // - //////////////////////////////////////// - { - WebhookEventSender sender = new WebhookEventSenderThatMocksHttp(new IOException("Connection Refused")); - assertFalse(sender.handleEvent(event, webhook, sendLogs)); - List insertedSendLogs = QueryAction.execute(WebhookEventSendLog.TABLE_NAME, sendLogFilter); - WebhookEventSendLog sendLog = new WebhookEventSendLog(insertedSendLogs.get(0)); - assertEquals("Connection Refused", sendLog.getErrorMessage()); - assertFalse(sendLog.getSuccessful()); - assertNull(sendLog.getHttpStatusCode()); - } - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testRateLimit() throws QException - { - ///////////////////////////////////////////////////////////////// - // use random ids to avoid everything being 1 and false-passes // - ///////////////////////////////////////////////////////////////// - Random random = new Random(); - Integer webhookId = insert(newWebhook("Test")); - Integer subscriptionId = insert(newWebhookSubscription(WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME).withWebhookId(webhookId)); - Integer eventId = insert(newWebhookEvent(new WebhookSubscription(GetAction.execute(WebhookSubscription.TABLE_NAME, subscriptionId)), WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME)); - - WebhookEvent event = new WebhookEvent(GetAction.execute(WebhookEvent.TABLE_NAME, eventId)); - event.setContent(List.of(new WebhookEventContent().withWebhookEventId(eventId).withPostBody("{}"))); - - Webhook webhook = new Webhook(GetAction.execute(Webhook.TABLE_NAME, webhookId)); - List sendLogs = new ArrayList<>(); - - { - /////////////////////////////////////////////////////////// - // use http mocker that will always fail with rate limit // - /////////////////////////////////////////////////////////// - long start = System.currentTimeMillis(); - WebhookEventSender sender = new WebhookEventSenderThatMocksHttp(429, "Too many requests"); - assertFalse(sender.handleEvent(event, webhook, sendLogs)); - List insertedSendLogs = QueryAction.execute(WebhookEventSendLog.TABLE_NAME, sendLogFilter); - WebhookEventSendLog sendLog = new WebhookEventSendLog(insertedSendLogs.get(0)); - - long end = System.currentTimeMillis(); - assertEquals("Giving up after too many rate-limit errors (3). Latest response: Too many requests", sendLog.getErrorMessage()); - assertFalse(sendLog.getSuccessful()); - assertEquals(429, sendLog.getHttpStatusCode()); - assertThat(end - start).isGreaterThan(WebhookEventSenderThatMocksHttp.BACKOFF_MILLIS * 3L); - } - - { - /////////////////////////////////////////////////////////////// - // use a sender that will rate-limit twice, but then succeed // - /////////////////////////////////////////////////////////////// - QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(WebhookEventSender.class); - long start = System.currentTimeMillis(); - WebhookEventSender sender = new RateLimitPassEventuallySender("Too fast"); - assertTrue(sender.handleEvent(event, webhook, sendLogs)); - List insertedSendLogs = QueryAction.execute(WebhookEventSendLog.TABLE_NAME, sendLogFilter); - WebhookEventSendLog sendLog = new WebhookEventSendLog(insertedSendLogs.get(0)); - QLogger.deactivateCollectingLoggerForClass(WebhookEventSender.class); - - long end = System.currentTimeMillis(); - assertNull(sendLog.getErrorMessage()); - assertTrue(sendLog.getSuccessful()); - assertEquals(200, sendLog.getHttpStatusCode()); - assertThat(end - start).isGreaterThan(WebhookEventSenderThatMocksHttp.BACKOFF_MILLIS * 3L); - assertThat(collectingLogger.getCollectedMessages()) - .anyMatch(m -> m.getMessage().contains("Caught rate limit. Will sleep and re-try")); - } - - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static class WebhookEventSenderThatFails extends WebhookEventSender - { - /*************************************************************************** - ** - ***************************************************************************/ - @Override - protected void doPost(WebhookEvent webhookEvent, Webhook webhook, WebhookEventSendLog sendLog) throws WebhookPostException - { - throw new WebhookPostException("Test failure"); - } - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static class WebhookEventSenderThatSucceeds extends WebhookEventSender - { - /*************************************************************************** - ** - ***************************************************************************/ - @Override - protected void doPost(WebhookEvent webhookEvent, Webhook webhook, WebhookEventSendLog sendLog) throws WebhookPostException - { - } - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - public static class WebhookEventSenderThatMocksHttp extends WebhookEventSender - { - protected Integer statusCode; - protected String responseBody; - private IOException e; - - public static final Integer BACKOFF_MILLIS = 3; - - - - /******************************************************************************* - ** Constructor - ** - *******************************************************************************/ - public WebhookEventSenderThatMocksHttp(int statusCode, String responseBody) - { - this.statusCode = statusCode; - this.responseBody = responseBody; - } - - - - /******************************************************************************* - ** Constructor - ** - *******************************************************************************/ - public WebhookEventSenderThatMocksHttp(IOException e) - { - this.e = e; - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - protected int getInitialRateLimitBackoffMillis() - { - return (BACKOFF_MILLIS); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - protected CloseableHttpClient buildHttpClient() - { - return new CloseableHttpClient() - { - @Override - protected CloseableHttpResponse doExecute(HttpHost httpHost, HttpRequest httpRequest, HttpContext httpContext) throws IOException, ClientProtocolException - { - return null; - } - - - - @Override - public void close() throws IOException - { - - } - - - - @Override - public HttpParams getParams() - { - return null; - } - - - - @Override - public ClientConnectionManager getConnectionManager() - { - return null; - } - }; - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - protected CloseableHttpResponse executeHttpRequest(CloseableHttpClient httpClient, HttpPost request) throws IOException - { - if(e != null) - { - throw (e); - } - - return new CloseableHttpResponse() - { - @Override - public void close() throws IOException - { - - } - - - - @Override - public StatusLine getStatusLine() - { - return new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), statusCode, "Yeah."); - } - - - - @Override - public void setStatusLine(StatusLine statusLine) - { - - } - - - - @Override - public void setStatusLine(ProtocolVersion protocolVersion, int i) - { - - } - - - - @Override - public void setStatusLine(ProtocolVersion protocolVersion, int i, String s) - { - - } - - - - @Override - public void setStatusCode(int i) throws IllegalStateException - { - - } - - - - @Override - public void setReasonPhrase(String s) throws IllegalStateException - { - - } - - - - @Override - public HttpEntity getEntity() - { - try - { - return new StringEntity(responseBody); - } - catch(UnsupportedEncodingException e) - { - throw new RuntimeException(e); - } - } - - - - @Override - public void setEntity(HttpEntity httpEntity) - { - - } - - - - @Override - public Locale getLocale() - { - return null; - } - - - - @Override - public void setLocale(Locale locale) - { - - } - - - - @Override - public ProtocolVersion getProtocolVersion() - { - return null; - } - - - - @Override - public boolean containsHeader(String s) - { - return false; - } - - - - @Override - public Header[] getHeaders(String s) - { - return new Header[0]; - } - - - - @Override - public Header getFirstHeader(String s) - { - return null; - } - - - - @Override - public Header getLastHeader(String s) - { - return null; - } - - - - @Override - public Header[] getAllHeaders() - { - return new Header[0]; - } - - - - @Override - public void addHeader(Header header) - { - - } - - - - @Override - public void addHeader(String s, String s1) - { - - } - - - - @Override - public void setHeader(Header header) - { - - } - - - - @Override - public void setHeader(String s, String s1) - { - - } - - - - @Override - public void setHeaders(Header[] headers) - { - - } - - - - @Override - public void removeHeader(Header header) - { - - } - - - - @Override - public void removeHeaders(String s) - { - - } - - - - @Override - public HeaderIterator headerIterator() - { - return null; - } - - - - @Override - public HeaderIterator headerIterator(String s) - { - return null; - } - - - - @Override - public HttpParams getParams() - { - return null; - } - - - - @Override - public void setParams(HttpParams httpParams) - { - - } - }; - } - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public static class RateLimitPassEventuallySender extends WebhookEventSenderThatMocksHttp - { - int count = 0; - - - - /******************************************************************************* - ** Constructor - ** - *******************************************************************************/ - public RateLimitPassEventuallySender(String message) - { - super(429, message); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - @Override - protected CloseableHttpResponse executeHttpRequest(CloseableHttpClient httpClient, HttpPost request) throws IOException - { - count++; - - if(count >= 3) - { - statusCode = 200; - responseBody = "OK"; - } - - return super.executeHttpRequest(httpClient, request); - } - } - -} \ No newline at end of file diff --git a/src/test/java/com/kingsrook/qbits/webhooks/actions/WebhookEventTypeCustomizerInterfaceTest.java b/src/test/java/com/kingsrook/qbits/webhooks/actions/WebhookEventTypeCustomizerInterfaceTest.java deleted file mode 100644 index 6031637..0000000 --- a/src/test/java/com/kingsrook/qbits/webhooks/actions/WebhookEventTypeCustomizerInterfaceTest.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.actions; - - -import com.kingsrook.qbits.webhooks.BaseTest; -import com.kingsrook.qbits.webhooks.WebhooksTestApplication; -import com.kingsrook.qbits.webhooks.model.WebhookEventContent; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import org.json.JSONObject; -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - - -/******************************************************************************* - ** Unit test for WebhookEventTypeCustomizerInterface - *******************************************************************************/ -class WebhookEventTypeCustomizerInterfaceTest extends BaseTest -{ - - /******************************************************************************* - ** the post body sent to receivers must keep fields whose value is null, so a - ** receiver can tell "field is null" from "field not in this api version". - *******************************************************************************/ - @Test - void testBuildEventContentKeepsNullValues() throws QException - { - String eventTypeName = "personAdHoc"; - registerAdHocEventType(eventTypeName); - - QRecord person = new QRecord() - .withTableName(WebhooksTestApplication.TABLE_NAME_PERSON) - .withValue("id", 1) - .withValue("firstName", "Jean Luc"); - - WebhookEventContent content = new DefaultWebhookEventTypeCustomizer() - .buildEventContent(person, eventTypeName, WebhooksTestApplication.API_NAME, WebhooksTestApplication.API_V1); - - JSONObject record = new JSONObject(content.getPostBody()).getJSONObject("record"); - assertEquals("Jean Luc", record.getString("firstName")); - assertTrue(record.has("lastName"), "null-valued field should be present in the post body"); - assertTrue(record.isNull("lastName")); - } - -} diff --git a/src/test/java/com/kingsrook/qbits/webhooks/actions/WebhookHealthManagerTest.java b/src/test/java/com/kingsrook/qbits/webhooks/actions/WebhookHealthManagerTest.java deleted file mode 100644 index c513e61..0000000 --- a/src/test/java/com/kingsrook/qbits/webhooks/actions/WebhookHealthManagerTest.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.actions; - - -import java.util.Stack; -import com.kingsrook.qbits.webhooks.BaseTest; -import com.kingsrook.qbits.webhooks.model.Webhook; -import com.kingsrook.qbits.webhooks.model.WebhookEventSendLog; -import com.kingsrook.qbits.webhooks.model.WebhookHealthStatus; -import com.kingsrook.qbits.webhooks.processes.SendWebhookEventLoadStep; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; - - -/******************************************************************************* - ** Unit test for WebhookHealthManager - *******************************************************************************/ -class WebhookHealthManagerTest extends BaseTest -{ - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void test() throws QException - { - Webhook webhook = newWebhook("test"); - webhook.setId(insert(webhook)); - - assertHealthStatus(WebhookHealthStatus.HEALTHY, webhook, makeStack(1)); - assertHealthStatus(WebhookHealthStatus.HEALTHY, webhook, makeStack(1, 1)); - assertHealthStatus(WebhookHealthStatus.HEALTHY, webhook, makeStack(0, 1, 1)); - assertHealthStatus(WebhookHealthStatus.HEALTHY, webhook, makeStack(0, 0, 0, 0, 0, 0, 0, 0, 0, 1)); - - Stack tenFails = makeStack(0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - assertHealthStatus(WebhookHealthStatus.UNHEALTHY, webhook, tenFails); - - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // make sure that pushing another event onto a stack that was full of fails, that it then lets a success go out of probation // - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - tenFails.push(new WebhookEventSendLog().withSuccessful(true)); - webhook.setHealthStatusId(WebhookHealthStatus.PROBATION.getId()); - assertHealthStatus(WebhookHealthStatus.HEALTHY, webhook, tenFails); - - webhook.setHealthStatusId(WebhookHealthStatus.PROBATION.getId()); - assertHealthStatus(WebhookHealthStatus.HEALTHY, webhook, makeStack(0, 1, 1, 1)); - - /////////////////////////////////////////////////////////////////// - // start webhook HEALTHy, and stack with 5 success, then 8 fails // - /////////////////////////////////////////////////////////////////// - webhook.setHealthStatusId(WebhookHealthStatus.HEALTHY.getId()); - Stack stack = makeStack(1); - for(int i = 0; i < 4; i++) - { - stack.push(new WebhookEventSendLog().withSuccessful(true)); - } - for(int i = 0; i < 8; i++) - { - stack.push(new WebhookEventSendLog().withSuccessful(false)); - } - - ////////////////// - // stay healthy // - ////////////////// - assertHealthStatus(WebhookHealthStatus.HEALTHY, webhook, stack); - - ///////////////////////////////////////// - // another fail (now 9), stays healthy // - ///////////////////////////////////////// - stack.push(new WebhookEventSendLog().withSuccessful(false)); - assertHealthStatus(WebhookHealthStatus.HEALTHY, webhook, stack); - - /////////////////////////////////////////// - // another fail (now 10), goes unhealthy // - /////////////////////////////////////////// - stack.push(new WebhookEventSendLog().withSuccessful(false)); - assertHealthStatus(WebhookHealthStatus.UNHEALTHY, webhook, stack); - - ///////////////////////////////////////////////////////////////////// - // now if we got to probation, then a success, and we'd go healthy // - ///////////////////////////////////////////////////////////////////// - webhook.setHealthStatusId(WebhookHealthStatus.PROBATION.getId()); - stack.push(new WebhookEventSendLog().withSuccessful(true)); - assertHealthStatus(WebhookHealthStatus.HEALTHY, webhook, stack); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - private void assertHealthStatus(WebhookHealthStatus expected, Webhook webhook, Stack recentSendLogs) throws QException - { - Webhook updatedWebhook = new WebhookHealthManager().updateWebhookHealth(recentSendLogs, webhook); - assertEquals(expected.getId(), updatedWebhook.getHealthStatusId()); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - private static Stack makeStack(int... values) - { - Stack rs = new SendWebhookEventLoadStep().newSendLogStack(10); - for(int value : values) - { - rs.add(new WebhookEventSendLog().withId(rs.size() + 1).withSuccessful(value == 1)); - } - return (rs); - } - -} \ No newline at end of file diff --git a/src/test/java/com/kingsrook/qbits/webhooks/actions/WebhookSubscriptionsHelperTest.java b/src/test/java/com/kingsrook/qbits/webhooks/actions/WebhookSubscriptionsHelperTest.java deleted file mode 100644 index 2bd9b9b..0000000 --- a/src/test/java/com/kingsrook/qbits/webhooks/actions/WebhookSubscriptionsHelperTest.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.actions; - - -import java.util.List; -import com.kingsrook.qbits.webhooks.BaseTest; -import com.kingsrook.qbits.webhooks.model.WebhookEventCategory; -import com.kingsrook.qbits.webhooks.model.WebhookEventCategory.Kind; -import com.kingsrook.qbits.webhooks.registry.WebhookEventType; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; - - -/******************************************************************************* - ** Unit test for IdentifyWebhookSubscriptions - *******************************************************************************/ -class WebhookSubscriptionsHelperTest extends BaseTest -{ - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void test() throws QException - { - String personTableName = "person"; - String placeTableName = "place"; - String insertedPersonEventName = "inserted-person"; - String insertedPlaceEventName = "inserted-place"; - String insertedPersonWithFirstNameEventName = "inserted-person-firstName"; - - ////////////////////////////////////////////// - // nothing exists yet, so all null or empty // - ////////////////////////////////////////////// - assertThat(getEventTypes(Kind.INSERT, "noSuchTable")).isNullOrEmpty(); - assertThat(getEventTypes(Kind.INSERT, "noSuchTable")).isNullOrEmpty(); - assertThat(getEventTypes(Kind.UPDATE, personTableName)).isNullOrEmpty(); - assertThat(getEventTypes(Kind.AD_HOC, personTableName)).isNullOrEmpty(); - - /////////////////////////////////////////////////////////////////////////////////////// - // register first event type, but no subscriptions to it, so still all empty results // - /////////////////////////////////////////////////////////////////////////////////////// - registerEventType(insertedPersonEventName, WebhookEventCategory.INSERT, personTableName); - assertThat(getEventTypes(Kind.INSERT, personTableName)).isNullOrEmpty(); - assertThat(getEventTypes(Kind.UPDATE, personTableName)).isNullOrEmpty(); - assertThat(getEventTypes(Kind.AD_HOC, personTableName)).isNullOrEmpty(); - - ////////////////////////////////////////////////////////////// - // add a second insert event with a sub on the person table // - ////////////////////////////////////////////////////////////// - registerEventType(insertedPersonWithFirstNameEventName, WebhookEventCategory.INSERT, personTableName, "firstName"); - insert(newWebhookSubscription(insertedPersonWithFirstNameEventName)); - WebhookSubscriptionsHelper.clearMemoizations(); - assertThat(getEventTypes(Kind.INSERT, personTableName)).hasSize(1); - assertThat(getEventTypes(Kind.UPDATE, personTableName)).isNullOrEmpty(); - assertThat(getEventTypes(Kind.AD_HOC, personTableName)).isNullOrEmpty(); - - //////////////////////////////////////// - // add insert event for another table // - //////////////////////////////////////// - registerEventType(insertedPlaceEventName, WebhookEventCategory.INSERT, placeTableName); - insert(newWebhookSubscription(insertedPlaceEventName)); - WebhookSubscriptionsHelper.clearMemoizations(); - assertThat(getEventTypes(Kind.INSERT, personTableName)).hasSize(1); - assertThat(getEventTypes(Kind.INSERT, placeTableName)).hasSize(1); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - private List getEventTypes(Kind kind, String tableName) - { - return WebhookSubscriptionsHelper.getWebhookEventTypesToConsiderFiringEventsFor(kind, tableName); - } - -} \ No newline at end of file diff --git a/src/test/java/com/kingsrook/qbits/webhooks/processes/ManageWebhookHealthTransformStepTest.java b/src/test/java/com/kingsrook/qbits/webhooks/processes/ManageWebhookHealthTransformStepTest.java deleted file mode 100644 index 0df8daa..0000000 --- a/src/test/java/com/kingsrook/qbits/webhooks/processes/ManageWebhookHealthTransformStepTest.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.processes; - - -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.List; -import com.kingsrook.qbits.webhooks.BaseTest; -import com.kingsrook.qbits.webhooks.model.Webhook; -import com.kingsrook.qbits.webhooks.model.WebhookEventSendLog; -import com.kingsrook.qbits.webhooks.model.WebhookHealthStatus; -import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; -import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine; -import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface; -import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; -import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; -import com.kingsrook.qqq.backend.core.model.actions.processes.Status; -import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; -import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; - - -/******************************************************************************* - ** Unit test for ManageWebhookHealthTransformStep - *******************************************************************************/ -class ManageWebhookHealthTransformStepTest extends BaseTest -{ - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void test() throws QException - { - Integer healthyId = insert(newWebhook("healthy").withHealthStatusId(WebhookHealthStatus.HEALTHY.getId())); - Integer onProbationId = insert(newWebhook("probation").withHealthStatusId(WebhookHealthStatus.PROBATION.getId())); - Integer unhealthyJustNowId = insert(newWebhook("unhealthyJustNow").withHealthStatusId(WebhookHealthStatus.UNHEALTHY.getId())); - Integer unhealthyLongAgoId = insert(newWebhook("unhealthyLongAgo").withHealthStatusId(WebhookHealthStatus.UNHEALTHY.getId())); - Integer unhealthyNoLogsId = insert(newWebhook("unhealthyNoLogs").withHealthStatusId(WebhookHealthStatus.UNHEALTHY.getId())); - - new InsertAction().execute(new InsertInput(WebhookEventSendLog.TABLE_NAME).withRecordEntities(List.of( - new WebhookEventSendLog().withWebhookId(unhealthyJustNowId).withStartTimestamp(Instant.now()), - new WebhookEventSendLog().withWebhookId(unhealthyLongAgoId).withStartTimestamp(Instant.now().minus(1, ChronoUnit.DAYS)) - ))); - - RunBackendStepInput input = new RunBackendStepInput(); - RunBackendStepOutput output = new RunBackendStepOutput(); - - input.withRecords(QueryAction.execute(Webhook.TABLE_NAME, new QQueryFilter())); - - ManageWebhookHealthTransformStep manageWebhookHealthTransformStep = new ManageWebhookHealthTransformStep(); - manageWebhookHealthTransformStep.runOnePage(input, output); - - assertEquals(1, output.getRecords().size()); - assertEquals(unhealthyLongAgoId, output.getRecords().get(0).getValueInteger("id")); - - ArrayList processSummaryLineInterfaces = manageWebhookHealthTransformStep.doGetProcessSummary(output, false); - assertEquals(3, processSummaryLineInterfaces.size()); - - assertEquals(Status.OK, processSummaryLineInterfaces.get(0).getStatus()); - assertEquals(1, ((ProcessSummaryLine)processSummaryLineInterfaces.get(0)).getCount()); - - assertEquals(Status.INFO, processSummaryLineInterfaces.get(1).getStatus()); - assertEquals(2, ((ProcessSummaryLine)processSummaryLineInterfaces.get(1)).getCount()); - assertThat(processSummaryLineInterfaces.get(1).getMessage()).contains("not currently Unhealthy"); - - assertEquals(Status.ERROR, processSummaryLineInterfaces.get(2).getStatus()); - assertEquals(2, ((ProcessSummaryLine)processSummaryLineInterfaces.get(2)).getCount()); - assertThat(processSummaryLineInterfaces.get(2).getMessage()).contains("Unhealthy long enough to be put on Probation"); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testNoUnhealthyInputs() throws QException - { - Integer healthyId = insert(newWebhook("healthy").withHealthStatusId(WebhookHealthStatus.HEALTHY.getId())); - - RunBackendStepInput input = new RunBackendStepInput(); - RunBackendStepOutput output = new RunBackendStepOutput(); - - input.withRecords(QueryAction.execute(Webhook.TABLE_NAME, new QQueryFilter())); - - ManageWebhookHealthTransformStep manageWebhookHealthTransformStep = new ManageWebhookHealthTransformStep(); - manageWebhookHealthTransformStep.runOnePage(input, output); - - assertEquals(0, output.getRecords().size()); - } - -} \ No newline at end of file diff --git a/src/test/java/com/kingsrook/qbits/webhooks/processes/SendTestEventToWebhookLoadStepTest.java b/src/test/java/com/kingsrook/qbits/webhooks/processes/SendTestEventToWebhookLoadStepTest.java deleted file mode 100644 index 35c2d9e..0000000 --- a/src/test/java/com/kingsrook/qbits/webhooks/processes/SendTestEventToWebhookLoadStepTest.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.processes; - - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import com.kingsrook.qbits.webhooks.BaseTest; -import com.kingsrook.qbits.webhooks.actions.WebhookEventSender; -import com.kingsrook.qbits.webhooks.actions.WebhookEventSenderTest; -import com.kingsrook.qbits.webhooks.model.Webhook; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine; -import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface; -import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; -import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; -import com.kingsrook.qqq.backend.core.model.actions.processes.Status; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; - - -/******************************************************************************* - ** Unit test for SendTestEventToWebhookLoadStep - *******************************************************************************/ -class SendTestEventToWebhookLoadStepTest extends BaseTest -{ - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testSuccess() throws QException - { - ProcessSummaryLine processSummaryLine = runProcessForSender(new WebhookEventSenderTest.WebhookEventSenderThatSucceeds(), 2); - assertEquals(Status.OK, processSummaryLine.getStatus()); - assertEquals("were delivered successfully", processSummaryLine.getMessage()); - assertEquals(2, processSummaryLine.getCount()); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testErrorWithoutStatusCode() throws QException - { - ProcessSummaryLine processSummaryLine = runProcessForSender(new WebhookEventSenderTest.WebhookEventSenderThatFails()); - assertEquals(Status.ERROR, processSummaryLine.getStatus()); - assertEquals("failed to send with error message: Test failure", processSummaryLine.getMessage()); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testErrorsRollup() throws QException - { - ProcessSummaryLine processSummaryLine = runProcessForSender(new WebhookEventSenderTest.WebhookEventSenderThatFails(), 3); - assertEquals(Status.ERROR, processSummaryLine.getStatus()); - assertEquals("failed to send with error message: Test failure", processSummaryLine.getMessage()); - assertEquals(3, processSummaryLine.getCount()); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testErrorWithStatusCode() throws QException - { - ProcessSummaryLine processSummaryLine = runProcessForSender(new WebhookEventSenderTest.WebhookEventSenderThatMocksHttp(500, "Server Error")); - assertEquals(Status.ERROR, processSummaryLine.getStatus()); - assertEquals("failed to send with error message: Status Code: 500; Server Error", processSummaryLine.getMessage()); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - private static ProcessSummaryLine runProcessForSender(WebhookEventSender webhookEventSender) throws QException - { - return runProcessForSender(webhookEventSender, 1); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - private static ProcessSummaryLine runProcessForSender(WebhookEventSender webhookEventSender, int nCopies) throws QException - { - SendTestEventToWebhookLoadStep step = getSendTestEventToWebhookLoadStep(webhookEventSender); - RunBackendStepOutput runBackendStepOutput = new RunBackendStepOutput(); - - List records = Collections.nCopies(nCopies, new Webhook().withUrl("http://localhost:8000/").toQRecord()); - - step.runOnePage(new RunBackendStepInput().withRecords(records), runBackendStepOutput); - ArrayList processSummary = step.doGetProcessSummary(runBackendStepOutput, true); - ProcessSummaryLine processSummaryLine = (ProcessSummaryLine) processSummary.get(0); - return processSummaryLine; - } - - - - /*************************************************************************** - * - ***************************************************************************/ - private static SendTestEventToWebhookLoadStep getSendTestEventToWebhookLoadStep(WebhookEventSender webhookEventSender) - { - SendTestEventToWebhookLoadStep step = new SendTestEventToWebhookLoadStep() - { - /*************************************************************************** - ** - ***************************************************************************/ - @Override - protected WebhookEventSender getWebhookEventSender() - { - return webhookEventSender; - } - }; - return step; - } - -} \ No newline at end of file diff --git a/src/test/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventExtractStepTest.java b/src/test/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventExtractStepTest.java deleted file mode 100644 index b05ecb8..0000000 --- a/src/test/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventExtractStepTest.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.processes; - - -import java.time.Instant; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.function.Consumer; -import com.kingsrook.qbits.webhooks.BaseTest; -import com.kingsrook.qbits.webhooks.WebhooksTestApplication; -import com.kingsrook.qbits.webhooks.model.Webhook; -import com.kingsrook.qbits.webhooks.model.WebhookActiveStatus; -import com.kingsrook.qbits.webhooks.model.WebhookEvent; -import com.kingsrook.qbits.webhooks.model.WebhookEventStatus; -import com.kingsrook.qbits.webhooks.model.WebhookHealthStatus; -import com.kingsrook.qbits.webhooks.model.WebhookSubscription; -import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; -import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; -import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; -import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep; -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - - -/******************************************************************************* - ** Unit test for SendWebhookEventExtractStep - *******************************************************************************/ -class SendWebhookEventExtractStepTest extends BaseTest -{ - - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testEventsFilteredByWebhook() throws QException - { - ///////////////////////////////////////////////////////////////////////////////////////////////////////////// - // make sure if there are valid events for multiple webhooks, that we only find the ones we're looking for // - ///////////////////////////////////////////////////////////////////////////////////////////////////////////// - Integer webhookId1 = 1; - Integer webhookId2 = 2; - List webhooks = new InsertAction().execute(new InsertInput(Webhook.TABLE_NAME).withRecordEntities(List.of( - new Webhook().withId(webhookId1).withName("a").withUrl("a").withSubscriptions(List.of(newWebhookSubscription(WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME))), - new Webhook().withId(webhookId2).withName("b").withUrl("b").withSubscriptions(List.of(newWebhookSubscription(WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME))) - ))).getRecords(); - - WebhookSubscription subscription1 = new WebhookSubscription(webhooks.get(0).getAssociatedRecords().get(Webhook.SUBSCRIPTIONS_ASSOCIATION_NAME).get(0)); - WebhookSubscription subscription2 = new WebhookSubscription(webhooks.get(1).getAssociatedRecords().get(Webhook.SUBSCRIPTIONS_ASSOCIATION_NAME).get(0)); - - new InsertAction().execute(new InsertInput(WebhookEvent.TABLE_NAME).withRecordEntities(List.of( - //////////////////////////// - // 3 events for webhook 1 // - //////////////////////////// - newWebhookEvent(subscription1, WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME).withEventSourceRecordId(11), - newWebhookEvent(subscription1, WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME).withEventSourceRecordId(12), - newWebhookEvent(subscription1, WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME).withEventSourceRecordId(13), - - //////////////////////////// - // 2 events for webhook 2 // - //////////////////////////// - newWebhookEvent(subscription2, WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME).withEventSourceRecordId(20), - newWebhookEvent(subscription2, WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME).withEventSourceRecordId(21) - ))); - - assertEquals(3, runStepForWebhookId(webhookId1).size()); - assertEquals(2, runStepForWebhookId(webhookId2).size()); - assertEquals(0, runStepForWebhookId(-1).size()); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testWebhookScenarios() throws QException - { - testWebhookScenario(true, new Webhook()); - testWebhookScenario(false, new Webhook().withActiveStatusId(WebhookActiveStatus.PAUSED.getId())); - testWebhookScenario(false, new Webhook().withActiveStatusId(WebhookActiveStatus.DISABLED.getId())); - testWebhookScenario(false, new Webhook().withHealthStatusId(WebhookHealthStatus.UNHEALTHY.getId())); - testWebhookScenario(true, new Webhook().withHealthStatusId(WebhookHealthStatus.PROBATION.getId())); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - private void testWebhookScenario(boolean expectToFindEvent, Webhook webhook) throws QException - { - QRecord insertedWebhook = new InsertAction().execute(new InsertInput(Webhook.TABLE_NAME).withRecordEntities(List.of(webhook - .withName(UUID.randomUUID().toString()) - .withUrl("a") - .withSubscriptions(List.of( - newWebhookSubscription(WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME) - ))))).getRecords().get(0); - - WebhookSubscription subscription = new WebhookSubscription(insertedWebhook.getAssociatedRecords().get(Webhook.SUBSCRIPTIONS_ASSOCIATION_NAME).get(0)); - - new InsertAction().execute(new InsertInput(WebhookEvent.TABLE_NAME).withRecordEntities(List.of( - newWebhookEvent(subscription, WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME).withEventSourceRecordId(100) - ))); - - List events = runStepForWebhookId(insertedWebhook.getValueInteger("id")); - if(expectToFindEvent) - { - assertFalse(events.isEmpty()); - } - else - { - assertTrue(events.isEmpty()); - } - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testWebhookEventScenarios() throws QException - { - Instant past = Instant.now().minusSeconds(60); - Instant future = Instant.now().plusSeconds(60); - - //////////////////////////////////////////////////////////////////////////////////////////// - // new should always match // - // though we could imagine a change in the future to allow new events to be future-dated? // - //////////////////////////////////////////////////////////////////////////////////////////// - testWebhookEventScenario(true, we -> we.withEventStatusId(WebhookEventStatus.NEW.getId())); - testWebhookEventScenario(true, we -> we.withEventStatusId(WebhookEventStatus.NEW.getId()).withNextAttemptTimestamp(future)); - - ////////////////////////////////////////////////////////////////////////////////////////////////// - // sending or awaiting retry - they both use nextAttemptTimestamp, so only true if in the past. // - ////////////////////////////////////////////////////////////////////////////////////////////////// - testWebhookEventScenario(false, we -> we.withEventStatusId(WebhookEventStatus.SENDING.getId())); - testWebhookEventScenario(true, we -> we.withEventStatusId(WebhookEventStatus.SENDING.getId()).withNextAttemptTimestamp(past)); - testWebhookEventScenario(false, we -> we.withEventStatusId(WebhookEventStatus.SENDING.getId()).withNextAttemptTimestamp(future)); - - testWebhookEventScenario(false, we -> we.withEventStatusId(WebhookEventStatus.AWAITING_RETRY.getId())); - testWebhookEventScenario(true, we -> we.withEventStatusId(WebhookEventStatus.AWAITING_RETRY.getId()).withNextAttemptTimestamp(past)); - testWebhookEventScenario(false, we -> we.withEventStatusId(WebhookEventStatus.AWAITING_RETRY.getId()).withNextAttemptTimestamp(future)); - - //////////////////////////////////////////////////////////////////////////////////// - // delivered and failed - should never be found, even if timestamp is in the past // - //////////////////////////////////////////////////////////////////////////////////// - testWebhookEventScenario(false, we -> we.withEventStatusId(WebhookEventStatus.DELIVERED.getId())); - testWebhookEventScenario(false, we -> we.withEventStatusId(WebhookEventStatus.DELIVERED.getId()).withNextAttemptTimestamp(past)); - - testWebhookEventScenario(false, we -> we.withEventStatusId(WebhookEventStatus.FAILED.getId())); - testWebhookEventScenario(false, we -> we.withEventStatusId(WebhookEventStatus.FAILED.getId()).withNextAttemptTimestamp(past)); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - private void testWebhookEventScenario(boolean expectToFindEvent, Consumer webhookEventCustomizer) throws QException - { - QRecord insertedWebhook = new InsertAction().execute(new InsertInput(Webhook.TABLE_NAME).withRecordEntities(List.of(new Webhook() - .withName(UUID.randomUUID().toString()) - .withUrl("a") - .withSubscriptions(List.of( - newWebhookSubscription(WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME) - ))))).getRecords().get(0); - - WebhookSubscription subscription = new WebhookSubscription(insertedWebhook.getAssociatedRecords().get(Webhook.SUBSCRIPTIONS_ASSOCIATION_NAME).get(0)); - - WebhookEvent webhookEvent = newWebhookEvent(subscription, WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME); - webhookEventCustomizer.accept(webhookEvent); - - new InsertAction().execute(new InsertInput(WebhookEvent.TABLE_NAME).withRecordEntities(List.of(webhookEvent))); - - List events = runStepForWebhookId(insertedWebhook.getValueInteger("id")); - if(expectToFindEvent) - { - assertFalse(events.isEmpty()); - } - else - { - assertTrue(events.isEmpty()); - } - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - private static List runStepForWebhookId(Integer webhookId1) throws QException - { - RunBackendStepInput input = new RunBackendStepInput().withValues(Map.of("webhookId", webhookId1, ExtractViaQueryStep.FIELD_SOURCE_TABLE, WebhookEvent.TABLE_NAME)); - RunBackendStepOutput output = new RunBackendStepOutput(); - - SendWebhookEventExtractStep sendWebhookEventExtractStep = new SendWebhookEventExtractStep(); - RecordPipe recordPipe = new RecordPipe(); - - sendWebhookEventExtractStep.setRecordPipe(recordPipe); - sendWebhookEventExtractStep.preRun(input, output); - sendWebhookEventExtractStep.run(input, output); - - List records = recordPipe.consumeAvailableRecords(); - return records; - } - -} \ No newline at end of file diff --git a/src/test/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventLoadStepTest.java b/src/test/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventLoadStepTest.java deleted file mode 100644 index a49d621..0000000 --- a/src/test/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventLoadStepTest.java +++ /dev/null @@ -1,280 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.processes; - - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import com.kingsrook.qbits.webhooks.BaseTest; -import com.kingsrook.qbits.webhooks.WebhooksTestApplication; -import com.kingsrook.qbits.webhooks.actions.WebhookEventSender; -import com.kingsrook.qbits.webhooks.actions.WebhookEventSenderTest; -import com.kingsrook.qbits.webhooks.model.Webhook; -import com.kingsrook.qbits.webhooks.model.WebhookEvent; -import com.kingsrook.qbits.webhooks.model.WebhookEventSendLog; -import com.kingsrook.qbits.webhooks.model.WebhookEventStatus; -import com.kingsrook.qbits.webhooks.model.WebhookHealthStatus; -import com.kingsrook.qbits.webhooks.model.WebhookSubscription; -import com.kingsrook.qqq.backend.core.actions.tables.GetAction; -import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; -import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine; -import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface; -import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; -import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; -import com.kingsrook.qqq.backend.core.model.actions.processes.Status; -import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; -import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; - - -/******************************************************************************* - ** Unit test for SendWebhookEventLoadStep - *******************************************************************************/ -class SendWebhookEventLoadStepTest extends BaseTest -{ - - /******************************************************************************* - * we had 12 fails on the books, so we'll have one more fail and go to unhealthy - *******************************************************************************/ - @Test - void testLast12failedThenAnotherFailed() throws QException - { - String eventTypeName = WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME; - - Integer webhookId0 = insert(newWebhook("test")); - Webhook webhook = new Webhook(GetAction.execute(Webhook.TABLE_NAME, webhookId0)); - - Integer subscriptionId = insert(newWebhookSubscription(eventTypeName).withWebhookId(webhookId0)); - WebhookSubscription webhookSubscription = new WebhookSubscription(GetAction.execute(WebhookSubscription.TABLE_NAME, subscriptionId)); - Integer webhookId = webhookSubscription.getWebhookId(); - - /////////////////////////////////////////// - // insert 3 events, each with 4 failures // - /////////////////////////////////////////// - Integer eventId0 = insertEventAwaitingRetry(webhookSubscription); - Integer eventId1 = insertEventAwaitingRetry(webhookSubscription); - Integer eventId2 = insertEventAwaitingRetry(webhookSubscription); - insertNFailLogsPerEvent(webhookId, 4, eventId0, eventId1, eventId2); - - ArrayList processSummaryLines = runProcess(webhook, new WebhookEventSenderTest.WebhookEventSenderThatFails(), eventId0, eventId1, eventId2); - - //////////////////////////////////////// - // make sure webhook became unhealthy // - //////////////////////////////////////// - assertEquals(WebhookHealthStatus.UNHEALTHY.getId(), GetAction.execute(Webhook.TABLE_NAME, webhookId).getValue("healthStatusId")); - - /////////////////////////////////////////////////////////////////////////////////////////////////////// - // only the first event should have been processed, because the webhook became unhealthy after that. // - /////////////////////////////////////////////////////////////////////////////////////////////////////// - assertWebhookEventStatus(WebhookEventStatus.FAILED, eventId0); - assertWebhookEventStatus(WebhookEventStatus.AWAITING_RETRY, eventId1); - assertWebhookEventStatus(WebhookEventStatus.AWAITING_RETRY, eventId2); - - assertEquals(2, processSummaryLines.size()); - - assertEquals(Status.ERROR, processSummaryLines.get(0).getStatus()); - assertEquals(1, ((ProcessSummaryLine) processSummaryLines.get(0)).getCount()); - assertThat(processSummaryLines.get(0).getMessage()).contains("failed to deliver"); - - assertEquals(Status.WARNING, processSummaryLines.get(1).getStatus()); - assertEquals(2, ((ProcessSummaryLine) processSummaryLines.get(1)).getCount()); - assertThat(processSummaryLines.get(1).getMessage()).contains("were not sent because their associated webhook became unhealthy."); - } - - - /******************************************************************************* - * we had 12 fails on the books, but now a success and we'll be successful - *******************************************************************************/ - @Test - void testLast12failedThenSuccess() throws QException - { - String eventTypeName = WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME; - - Integer webhookId0 = insert(newWebhook("test").withHealthStatusId(WebhookHealthStatus.PROBATION.getId())); - Webhook webhook = new Webhook(GetAction.execute(Webhook.TABLE_NAME, webhookId0)); - - Integer subscriptionId = insert(newWebhookSubscription(eventTypeName).withWebhookId(webhookId0)); - WebhookSubscription webhookSubscription = new WebhookSubscription(GetAction.execute(WebhookSubscription.TABLE_NAME, subscriptionId)); - Integer webhookId = webhookSubscription.getWebhookId(); - - /////////////////////////////////////////// - // insert 3 events, each with 4 failures // - /////////////////////////////////////////// - Integer eventId0 = insertEventAwaitingRetry(webhookSubscription); - Integer eventId1 = insertEventAwaitingRetry(webhookSubscription); - Integer eventId2 = insertEventAwaitingRetry(webhookSubscription); - insertNFailLogsPerEvent(webhookId, 4, eventId0, eventId1, eventId2); - - ArrayList processSummaryLines = runProcess(webhook, new WebhookEventSenderTest.WebhookEventSenderThatSucceeds(), eventId0, eventId1, eventId2); - - ////////////////////////////////////// - // make sure webhook became healthy // - ////////////////////////////////////// - assertEquals(WebhookHealthStatus.HEALTHY.getId(), GetAction.execute(Webhook.TABLE_NAME, webhookId).getValue("healthStatusId")); - - /////////////////////////////////////////////////////////////////////////////////////////////////////// - // only the first event should have been processed, because the webhook became unhealthy after that. // - /////////////////////////////////////////////////////////////////////////////////////////////////////// - assertWebhookEventStatus(WebhookEventStatus.DELIVERED, eventId0); - assertWebhookEventStatus(WebhookEventStatus.DELIVERED, eventId1); - assertWebhookEventStatus(WebhookEventStatus.DELIVERED, eventId2); - - assertEquals(1, processSummaryLines.size()); - - assertEquals(Status.OK, processSummaryLines.get(0).getStatus()); - assertEquals(3, ((ProcessSummaryLine) processSummaryLines.get(0)).getCount()); - assertThat(processSummaryLines.get(0).getMessage()).contains("were delivered"); - } - - - - /******************************************************************************* - * we have 6 fails on the books - now all 3 will fail, but not enough to go unhealthy - *******************************************************************************/ - @Test - void testLast6failed() throws QException - { - String eventTypeName = WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME; - - Integer webhookId0 = insert(newWebhook("test")); - Webhook webhook = new Webhook(GetAction.execute(Webhook.TABLE_NAME, webhookId0)); - - Integer subscriptionId = insert(newWebhookSubscription(eventTypeName).withWebhookId(webhookId0)); - WebhookSubscription webhookSubscription = new WebhookSubscription(GetAction.execute(WebhookSubscription.TABLE_NAME, subscriptionId)); - Integer webhookId = webhookSubscription.getWebhookId(); - - /////////////////////////////////////////// - // insert 3 events, each with 2 failures // - /////////////////////////////////////////// - Integer eventId0 = insertEventAwaitingRetry(webhookSubscription); - Integer eventId1 = insertEventAwaitingRetry(webhookSubscription); - Integer eventId2 = insertEventAwaitingRetry(webhookSubscription); - insertNFailLogsPerEvent(webhookId, 2, eventId0, eventId1, eventId2); - - ArrayList processSummaryLines = runProcess(webhook, new WebhookEventSenderTest.WebhookEventSenderThatFails(), eventId0, eventId1, eventId2); - - //////////////////////////////////////// - // make sure webhook is still healthy // - //////////////////////////////////////// - assertEquals(WebhookHealthStatus.HEALTHY.getId(), GetAction.execute(Webhook.TABLE_NAME, webhookId).getValue("healthStatusId")); - - ////////////////////////////////////////// - // all 3 should still be awaiting retry // - ////////////////////////////////////////// - assertWebhookEventStatus(WebhookEventStatus.AWAITING_RETRY, eventId0); - assertWebhookEventStatus(WebhookEventStatus.AWAITING_RETRY, eventId1); - assertWebhookEventStatus(WebhookEventStatus.AWAITING_RETRY, eventId2); - - assertEquals(1, processSummaryLines.size()); - - assertEquals(Status.ERROR, processSummaryLines.get(0).getStatus()); - assertEquals(3, ((ProcessSummaryLine) processSummaryLines.get(0)).getCount()); - assertThat(processSummaryLines.get(0).getMessage()).contains("failed to deliver"); - } - - - - /*************************************************************************** - * - ***************************************************************************/ - private void assertWebhookEventStatus(WebhookEventStatus webhookEventStatus, Integer eventId) throws QException - { - assertEquals(webhookEventStatus.getId(), GetAction.execute(WebhookEvent.TABLE_NAME, eventId).getValue("eventStatusId")); - } - - - - /*************************************************************************** - * - ***************************************************************************/ - private static ArrayList runProcess(Webhook webhook, WebhookEventSender webhookEventSender, Integer... eventIds) throws QException - { - SendWebhookEventLoadStep loadStep = getSendWebhookEventLoadStep(webhookEventSender); - loadStep.setTransformStep(new SendWebhookEventTransformStep().withWebhook(webhook)); - - ////////////////////////////////// - // run process for these events // - ////////////////////////////////// - RunBackendStepInput input = new RunBackendStepInput(); - input.withRecords(QueryAction.execute(WebhookEvent.TABLE_NAME, new QQueryFilter() - .withCriteria(new QFilterCriteria("id", QCriteriaOperator.IN, Arrays.stream(eventIds).toList())) - .withOrderBy(new QFilterOrderBy("id")))); - RunBackendStepOutput output = new RunBackendStepOutput(); - loadStep.preRun(input, output); - loadStep.runOnePage(input, output); - ArrayList processSummaryLines = loadStep.doGetProcessSummary(output, true); - return processSummaryLines; - } - - - - /*************************************************************************** - * - ***************************************************************************/ - private static void insertNFailLogsPerEvent(Integer webhookId, int n, Integer... eventIds) throws QException - { - for(Integer eventId : eventIds) - { - new InsertAction().execute(new InsertInput(WebhookEventSendLog.TABLE_NAME).withRecordEntities(Collections.nCopies(n, - new WebhookEventSendLog().withWebhookId(webhookId).withWebhookEventId(eventId).withSuccessful(false)))); - } - } - - - - /*************************************************************************** - * - ***************************************************************************/ - private static Integer insertEventAwaitingRetry(WebhookSubscription webhookSubscription) throws QException - { - return insert(newWebhookEvent(webhookSubscription, webhookSubscription.getWebhookEventTypeName()).withEventStatusId(WebhookEventStatus.AWAITING_RETRY.getId())); - } - - - - /*************************************************************************** - * - ***************************************************************************/ - private static SendWebhookEventLoadStep getSendWebhookEventLoadStep(WebhookEventSender webhookEventSender) - { - SendWebhookEventLoadStep loadStep = new SendWebhookEventLoadStep() - { - /*************************************************************************** - ** - ***************************************************************************/ - @Override - protected WebhookEventSender newWebhookEventSender() - { - return webhookEventSender; - } - }; - return loadStep; - } - -} \ No newline at end of file diff --git a/src/test/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventTransformStepTest.java b/src/test/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventTransformStepTest.java deleted file mode 100644 index 926e409..0000000 --- a/src/test/java/com/kingsrook/qbits/webhooks/processes/SendWebhookEventTransformStepTest.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.processes; - - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; -import java.util.function.Consumer; -import com.kingsrook.qbits.webhooks.BaseTest; -import com.kingsrook.qbits.webhooks.WebhooksTestApplication; -import com.kingsrook.qbits.webhooks.model.Webhook; -import com.kingsrook.qbits.webhooks.model.WebhookActiveStatus; -import com.kingsrook.qbits.webhooks.model.WebhookEvent; -import com.kingsrook.qbits.webhooks.model.WebhookEventStatus; -import com.kingsrook.qbits.webhooks.model.WebhookHealthStatus; -import com.kingsrook.qbits.webhooks.model.WebhookSubscription; -import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface; -import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; -import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; -import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; - - -/******************************************************************************* - ** Unit test for SendWebhookEventTransformStep - *******************************************************************************/ -class SendWebhookEventTransformStepTest extends BaseTest -{ - - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void test() throws QException - { - /////////////////////////////////////// - // everything "normal", will be sent // - /////////////////////////////////////// - testOneScenario("will be sent", null, null, null); - - //////////////////////////// - // missing webhook errors // - //////////////////////////// - testOneScenario("webhook was not found", e -> e.setWebhookId(-1), null, null); - testOneScenario("webhook was not found", e -> e.setWebhookId(null), null, null); - - ///////////////////////////////// - // missing subscription errors // - ///////////////////////////////// - testOneScenario("subscription was not found", e -> e.setWebhookSubscriptionId(-1), null, null); - testOneScenario("subscription was not found", e -> e.setWebhookSubscriptionId(null), null, null); - - ///////////////////////////////// - // disabled things do not send // - ///////////////////////////////// - testOneScenario("webhook is disabled", null, w -> w.setActiveStatusId(WebhookActiveStatus.DISABLED.getId()), null); - testOneScenario("subscription is disabled", null, null, s -> s.setActiveStatusId(WebhookActiveStatus.DISABLED.getId())); - - //////////////////////////////////////////////////////////////////////////////////////////////// - // paused one do - if they were manually pulled into the process (extract step excludes them) // - //////////////////////////////////////////////////////////////////////////////////////////////// - testOneScenario("will be sent", null, w -> w.setActiveStatusId(WebhookActiveStatus.PAUSED.getId()), null); - testOneScenario("will be sent", null, null, s -> s.setActiveStatusId(WebhookActiveStatus.PAUSED.getId())); - - ////////////////////////////////////////////////////// - // unhealthy webhook does not send - probation does // - ////////////////////////////////////////////////////// - testOneScenario("webhook is unhealthy", null, w -> w.setHealthStatusId(WebhookHealthStatus.UNHEALTHY.getId()), null); - testOneScenario("will be sent", null, w -> w.setHealthStatusId(WebhookHealthStatus.PROBATION.getId()), null); - - ///////////////////////////// - // already-delivered warns // - ///////////////////////////// - testOneScenario("will be sent again", e -> e.setEventStatusId(WebhookEventStatus.DELIVERED.getId()), null, null); - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - private ArrayList testOneScenario(String expectedSummaryLineToContain, Consumer eventCustomizer, Consumer webhookCustomizer, Consumer subscriptionCustomizer) throws QException - { - Webhook webhook = new Webhook() - .withName(UUID.randomUUID().toString()) - .withUrl("a"); - if(webhookCustomizer != null) - { - webhookCustomizer.accept(webhook); - } - QRecord insertedWebhook = new InsertAction().execute(new InsertInput(Webhook.TABLE_NAME).withRecordEntities(List.of(webhook))).getRecords().get(0); - - WebhookSubscription subscription = newWebhookSubscription(WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME) - .withWebhookId(insertedWebhook.getValueInteger("id")); - if(subscriptionCustomizer != null) - { - subscriptionCustomizer.accept(subscription); - } - WebhookSubscription insertedSubscription = new WebhookSubscription(new InsertAction().execute(new InsertInput(WebhookSubscription.TABLE_NAME).withRecordEntities(List.of(subscription))).getRecords().get(0)); - - WebhookEvent webhookEvent = newWebhookEvent(insertedSubscription, WebhooksTestApplication.PERSON_INSERTED_EVENT_TYPE_NAME); - if(eventCustomizer != null) - { - eventCustomizer.accept(webhookEvent); - } - QRecord insertedEvent = new InsertAction().execute(new InsertInput(WebhookEvent.TABLE_NAME).withRecordEntities(List.of(webhookEvent))).getRecords().get(0); - - RunBackendStepInput input = new RunBackendStepInput(); - RunBackendStepOutput output = new RunBackendStepOutput(); - - input.setRecords(List.of(insertedEvent)); - - SendWebhookEventTransformStep sendWebhookEventTransformStep = new SendWebhookEventTransformStep(); - sendWebhookEventTransformStep.preRun(input, output); - sendWebhookEventTransformStep.runOnePage(input, output); - - ArrayList processSummaryLineInterfaces = sendWebhookEventTransformStep.doGetProcessSummary(output, false); - assertEquals(1, processSummaryLineInterfaces.size()); - assertThat(processSummaryLineInterfaces.get(0).getMessage()).contains(expectedSummaryLineToContain); - - return processSummaryLineInterfaces; - } - -} \ No newline at end of file diff --git a/src/test/java/com/kingsrook/qbits/webhooks/registry/WebhooksRegistryValidationAndEnrichmentTest.java b/src/test/java/com/kingsrook/qbits/webhooks/registry/WebhooksRegistryValidationAndEnrichmentTest.java deleted file mode 100644 index e7e9519..0000000 --- a/src/test/java/com/kingsrook/qbits/webhooks/registry/WebhooksRegistryValidationAndEnrichmentTest.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.kingsrook.qbits.webhooks.registry; - - -import java.util.List; -import java.util.function.Consumer; -import com.kingsrook.qbits.webhooks.BaseTest; -import com.kingsrook.qbits.webhooks.WebhooksTestApplication; -import com.kingsrook.qbits.webhooks.actions.DefaultWebhookEventTypeCustomizer; -import com.kingsrook.qbits.webhooks.model.WebhookEventCategory; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; -import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; -import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.fail; - - -/******************************************************************************* - ** Unit test for validation in the webhooks registry - *******************************************************************************/ -public class WebhooksRegistryValidationAndEnrichmentTest extends BaseTest -{ - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testValidation() throws QException - { - assertValidationFailureReasons( - (qInstance -> WebhooksRegistry.ofOrWithNew(qInstance).registerWebhookEventType(new WebhookEventType())), - "Webhook Event Type: null is missing a name", - "Webhook Event Type: null is missing a category" - ); - - assertValidationFailureReasons( - (qInstance -> WebhooksRegistry.ofOrWithNew(qInstance).registerWebhookEventType(new WebhookEventType() - .withName("myEventType") - .withCategory(WebhookEventCategory.INSERT) - )), - "Webhook Event Type: myEventType requires a tableName, but does not have one" - ); - - assertValidationFailureReasons( - (qInstance -> WebhooksRegistry.ofOrWithNew(qInstance).registerWebhookEventType(new WebhookEventType() - .withName("myEventType") - .withCategory(WebhookEventCategory.INSERT) - .withTableName("nonTable") - )), - "Webhook Event Type: myEventType references a non-existing tableName: nonTable" - ); - - assertValidationFailureReasons( - (qInstance -> WebhooksRegistry.ofOrWithNew(qInstance).registerWebhookEventType(new WebhookEventType() - .withName("myEventType") - .withCategory(WebhookEventCategory.INSERT_WITH_FIELD) - .withTableName(WebhooksTestApplication.TABLE_NAME_PERSON) - )), - "Webhook Event Type: myEventType requires a fieldName, but does not have one" - ); - - assertValidationFailureReasons( - (qInstance -> WebhooksRegistry.ofOrWithNew(qInstance).registerWebhookEventType(new WebhookEventType() - .withName("myEventType") - .withCategory(WebhookEventCategory.INSERT_WITH_FIELD) - .withTableName(WebhooksTestApplication.TABLE_NAME_PERSON) - .withFieldName("nonField") - )), - "Webhook Event Type: myEventType references a non-existing fieldName: nonField for table person" - ); - - assertValidationFailureReasons( - (qInstance -> WebhooksRegistry.ofOrWithNew(qInstance).registerWebhookEventType(new WebhookEventType() - .withName("myEventType") - .withCategory(WebhookEventCategory.INSERT_WITH_VALUE) - .withTableName(WebhooksTestApplication.TABLE_NAME_PERSON) - .withFieldName("firstName") - )), - "Webhook Event Type: myEventType requires a value, but does not have one" - ); - - assertValidationSuccess( - (qInstance -> WebhooksRegistry.ofOrWithNew(qInstance).registerWebhookEventType(new WebhookEventType() - .withName("myEventType") - .withCategory(WebhookEventCategory.INSERT_WITH_VALUE) - .withTableName(WebhooksTestApplication.TABLE_NAME_PERSON) - .withFieldName("firstName") - .withValue("Jean-Luc") - ))); - - assertValidationFailureReasons( - (qInstance -> WebhooksRegistry.ofOrWithNew(qInstance).registerWebhookEventType(new WebhookEventType() - .withName("myEventType") - .withCategory(WebhookEventCategory.AD_HOC) - .withCustomizer(new QCodeReference(getClass())) - )), - "Webhook Event Type: myEventType customizer codeReference: CodeReference is not of the expected type: interface com.kingsrook.qbits.webhooks.actions.WebhookEventTypeCustomizerInterface" - ); - - assertValidationSuccess( - (qInstance -> WebhooksRegistry.ofOrWithNew(qInstance).registerWebhookEventType(new WebhookEventType() - .withName("myEventType") - .withCategory(WebhookEventCategory.AD_HOC) - .withCustomizer(new QCodeReference(DefaultWebhookEventTypeCustomizer.class)) - ))); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testEnrichment() throws QException - { - QInstance enrichedInstance = assertValidationSuccess( - (qInstance -> WebhooksRegistry.ofOrWithNew(qInstance).registerWebhookEventType(new WebhookEventType() - .withName("myEventType") - .withCategory(WebhookEventCategory.AD_HOC) - ))); - assertEquals("My Event Type", WebhooksRegistry.of(enrichedInstance).getWebhookEventType("myEventType").getLabel()); - - QInstance didntNeedEnrichedInstance = assertValidationSuccess( - (qInstance -> WebhooksRegistry.ofOrWithNew(qInstance).registerWebhookEventType(new WebhookEventType() - .withName("myEventType") - .withLabel("My Label") - .withCategory(WebhookEventCategory.AD_HOC) - ))); - assertEquals("My Label", WebhooksRegistry.of(didntNeedEnrichedInstance).getWebhookEventType("myEventType").getLabel()); - } - - - - /******************************************************************************* - ** Assert that an instance is valid! - *******************************************************************************/ - public static QInstance assertValidationSuccess(Consumer setup) throws QException - { - try - { - QInstance qInstance = new WebhooksTestApplication().defineQInstance(); - setup.accept(qInstance); - new QInstanceValidator().validate(qInstance); - return (qInstance); - } - catch(QInstanceValidationException e) - { - fail("Expected no validation errors, but received: " + e.getMessage()); - return null; - } - } - - - - /*************************************************************************** - ** - ***************************************************************************/ - private static void assertValidationFailureReasons(Consumer setup, String... expectedReasons) throws QException - { - try - { - QInstance qInstance = new WebhooksTestApplication().defineQInstance(); - setup.accept(qInstance); - new QInstanceValidator().validate(qInstance); - fail("Should have thrown validationException"); - } - catch(QInstanceValidationException e) - { - assertValidationFailureReasons(false, e.getReasons(), expectedReasons); - } - - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public static void assertValidationFailureReasons(boolean allowExtraReasons, List actualReasons, String... expectedReasons) - { - if(!allowExtraReasons) - { - int noOfReasons = actualReasons == null ? 0 : actualReasons.size(); - assertEquals(expectedReasons.length, noOfReasons, "Expected number of validation failure reasons.\nExpected reasons: " + String.join(",", expectedReasons) - + "\nActual reasons: " + (noOfReasons > 0 ? String.join("\n", actualReasons) : "--")); - } - - for(String reason : expectedReasons) - { - assertReason(reason, actualReasons); - } - } - - - - /******************************************************************************* - ** utility method for asserting that a specific reason string is found within - ** the list of reasons in the QInstanceValidationException. - ** - *******************************************************************************/ - public static void assertReason(String reason, List actualReasons) - { - assertNotNull(actualReasons, "Expected there to be a reason for the failure (but there was not)"); - assertThat(actualReasons) - .withFailMessage("Expected any of:\n%s\nTo match: [%s]", actualReasons, reason) - .anyMatch(s -> s.contains(reason)); - } - -} \ No newline at end of file -- 2.54.0