chore!: remove the 0.x webhooks sources and tests #11

Merged
KofTwentyTwo merged 1 commits from chore/remove-0x-sources into develop 2026-10-12 23:26:09 +00:00
57 changed files with 0 additions and 10047 deletions

No files matched your search

-6
View File
@@ -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.
-6
View File
@@ -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.
-32
View File
@@ -128,36 +128,6 @@
<build>
<plugins>
<!-- TEMPORARY (1.0 PR): the 0.x sources stay in the tree until their removal is approved; they are not compiled. -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<excludes>
<exclude>com/kingsrook/qbits/webhooks/actions/**</exclude>
<exclude>com/kingsrook/qbits/webhooks/model/**</exclude>
<exclude>com/kingsrook/qbits/webhooks/tables/**</exclude>
<exclude>com/kingsrook/qbits/webhooks/registry/WebhooksRegistryEnricherPlugin.java</exclude>
<exclude>com/kingsrook/qbits/webhooks/registry/WebhooksRegistryValidatorPlugin.java</exclude>
<exclude>com/kingsrook/qbits/webhooks/processes/ManageWebhookHealth*.java</exclude>
<exclude>com/kingsrook/qbits/webhooks/processes/SendTestEventToWebhook*.java</exclude>
<exclude>com/kingsrook/qbits/webhooks/processes/SendWebhookEvent*.java</exclude>
<exclude>com/kingsrook/qbits/webhooks/processes/SyncWebhookScheduledJobProcess.java</exclude>
</excludes>
<testExcludes>
<testExclude>com/kingsrook/qbits/webhooks/BaseTest.java</testExclude>
<testExclude>com/kingsrook/qbits/webhooks/WebhooksQBitConfigTest.java</testExclude>
<testExclude>com/kingsrook/qbits/webhooks/WebhooksTestApplication.java</testExclude>
<testExclude>com/kingsrook/qbits/webhooks/WebhooksTestJavalinServer.java</testExclude>
<testExclude>com/kingsrook/qbits/webhooks/WebhooksTestReceiver*.java</testExclude>
<testExclude>com/kingsrook/qbits/webhooks/actions/**</testExclude>
<testExclude>com/kingsrook/qbits/webhooks/processes/ManageWebhookHealthTransformStepTest.java</testExclude>
<testExclude>com/kingsrook/qbits/webhooks/processes/SendTestEventToWebhookLoadStepTest.java</testExclude>
<testExclude>com/kingsrook/qbits/webhooks/processes/SendWebhookEvent*Test.java</testExclude>
<testExclude>com/kingsrook/qbits/webhooks/registry/WebhooksRegistryValidationAndEnrichmentTest.java</testExclude>
</testExcludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
@@ -178,8 +148,6 @@
<failOnViolation>true</failOnViolation>
<violationSeverity>warning</violationSeverity>
<includeTestSourceDirectory>false</includeTestSourceDirectory>
<!-- TEMPORARY (1.0 PR): the uncompiled 0.x sources, until their removal is approved. -->
<excludes>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</excludes>
</configuration>
<executions>
<execution>
@@ -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<QRecord> postInsertOrUpdate(AbstractActionInput input, List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
{
WebhookSubscriptionsHelper.clearMemoizations();;
return (records);
}
/***************************************************************************
**
***************************************************************************/
@Override
public List<QRecord> postDelete(DeleteInput deleteInput, List<QRecord> records) throws QException
{
WebhookSubscriptionsHelper.clearMemoizations();;
return (records);
}
}
@@ -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
{
}
@@ -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<WebhookEventType> 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);
}
}
}
@@ -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<WebhookSubscription> 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;
}
}
@@ -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<QRecord> postInsert(InsertInput insertInput, List<QRecord> 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<WebhookEventType> insertWebhookEventTypes = WebhookSubscriptionsHelper.getWebhookEventTypesToConsiderFiringEventsFor(WebhookEventCategory.Kind.INSERT, insertInput.getTableName());
List<WebhookEventType> storeWebhookEventTypes = WebhookSubscriptionsHelper.getWebhookEventTypesToConsiderFiringEventsFor(WebhookEventCategory.Kind.STORE, insertInput.getTableName());
if(CollectionUtils.nullSafeIsEmpty(insertWebhookEventTypes) && CollectionUtils.nullSafeIsEmpty(storeWebhookEventTypes))
{
return (records);
}
List<WebhookEventType> 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);
}
}
@@ -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<QRecord> postUpdate(UpdateInput updateInput, List<QRecord> records, Optional<List<QRecord>> 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<WebhookEventType> updateWebhookEventTypes = WebhookSubscriptionsHelper.getWebhookEventTypesToConsiderFiringEventsFor(WebhookEventCategory.Kind.UPDATE, updateInput.getTableName());
List<WebhookEventType> storeWebhookEventTypes = WebhookSubscriptionsHelper.getWebhookEventTypesToConsiderFiringEventsFor(WebhookEventCategory.Kind.STORE, updateInput.getTableName());
if(CollectionUtils.nullSafeIsEmpty(updateWebhookEventTypes) && CollectionUtils.nullSafeIsEmpty(storeWebhookEventTypes))
{
return (records);
}
List<WebhookEventType> 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<QRecord> 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<String, Serializable> 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<QRecord> 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);
}
}
@@ -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<QRecord> 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));
}
}
}
@@ -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<WebhookEventSendLog> 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<QRecord> 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<Integer> configValue = WebhooksQBitConfig.getConfigValue(config -> config.getMinutesBetweenRetryAttempts());
backoffMinutes = configValue.toArray(new Integer[0]);
}
return (backoffMinutes);
}
}
@@ -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<String, Object> postBody = new LinkedHashMap<>();
Map<String, Object> 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<String, Serializable> 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<String, Object> postBody) throws QException
{
/////////////////////
// noop by default //
/////////////////////
}
}
@@ -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<WebhookEventSendLog> 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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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<String, List<WebhookSubscription>> subscriptionsByEventTypeMemoization = new Memoization<String, List<WebhookSubscription>>()
.withTimeout(Duration.of(WebhooksQBitConfig.getConfigValue(config -> config.getMemoizationTimeoutMinutes()).longValue(), ChronoUnit.MINUTES));
private static final Memoization<Integer, QRecord> webhookBySubscriptionIdMemoization = new Memoization<Integer, QRecord>()
.withTimeout(Duration.of(WebhooksQBitConfig.getConfigValue(config -> config.getMemoizationTimeoutMinutes()).longValue(), ChronoUnit.MINUTES));
private static List<String> allAccessSecurityKeysToUse = null;
/***************************************************************************
**
***************************************************************************/
public static void clearMemoizations()
{
subscriptionsByEventTypeMemoization.clear();
webhookBySubscriptionIdMemoization.clear();
}
/***************************************************************************
**
***************************************************************************/
public static List<WebhookEventType> getWebhookEventTypesToConsiderFiringEventsFor(WebhookEventCategory.Kind kind, String tableName)
{
List<WebhookEventType> 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<WebhookSubscription> 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<QRecord> 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<String> getAllAccessSecurityKeysToUse()
{
if(allAccessSecurityKeysToUse == null)
{
List<String> list = new ArrayList<>();
Map<String, String> 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<RecordSecurityLock> 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));
}
}
@@ -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<QTableMetaData>
{
/***************************************************************************
**
***************************************************************************/
@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<QWidgetMetaData>
{
/***************************************************************************
**
***************************************************************************/
@Override
public QWidgetMetaData customizeMetaData(QInstance qInstance, QWidgetMetaData widget) throws QException
{
////////////////////////////
// sort events by id desc //
////////////////////////////
ArrayList<QFilterOrderBy> 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<WebhookSubscription> 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<WebhookSubscription> getSubscriptions()
{
return (this.subscriptions);
}
/*******************************************************************************
** Setter for subscriptions
*******************************************************************************/
public void setSubscriptions(List<WebhookSubscription> subscriptions)
{
this.subscriptions = subscriptions;
}
/*******************************************************************************
** Fluent setter for subscriptions
*******************************************************************************/
public Webhook withSubscriptions(List<WebhookSubscription> 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);
}
}
@@ -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<Integer>
{
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());
}
}
@@ -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<QTableMetaData>
{
/***************************************************************************
**
***************************************************************************/
@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<WebhookEventContent> 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<WebhookEventContent> getContent()
{
return (this.content);
}
/*******************************************************************************
** Setter for content
*******************************************************************************/
public void setContent(List<WebhookEventContent> content)
{
this.content = content;
}
/*******************************************************************************
** Fluent setter for content
*******************************************************************************/
public WebhookEvent withContent(List<WebhookEventContent> 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);
}
}
@@ -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<String>
{
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;
}
}
@@ -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<QTableMetaData>
{
/***************************************************************************
**
***************************************************************************/
@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);
}
}
@@ -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<QTableMetaData>
{
/***************************************************************************
**
***************************************************************************/
@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);
}
}
@@ -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<Integer>
{
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());
}
}
@@ -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<Integer>
{
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());
}
}
@@ -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<QTableMetaData>
{
/***************************************************************************
**
***************************************************************************/
@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<QWidgetMetaData>
{
/***************************************************************************
**
***************************************************************************/
@Override
public QWidgetMetaData customizeMetaData(QInstance qInstance, QWidgetMetaData widget) throws QException
{
////////////////////////////
// sort events by id desc //
////////////////////////////
ArrayList<QFilterOrderBy> 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);
}
}
@@ -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
}
@@ -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<QProcessMetaData>
{
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);
}
}
@@ -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<ProcessSummaryLineInterface> getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen)
{
ArrayList<ProcessSummaryLineInterface> 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<Webhook> 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<WebhookEventSendLog> 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());
}
}
}
}
@@ -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<ProcessSummaryLineInterface> getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen)
{
ArrayList<ProcessSummaryLineInterface> 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<QRecord> 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();
}
}
@@ -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<QProcessMetaData>
{
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);
}
}
@@ -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);
}
}
@@ -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<WebhookEventSendLog> 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<ProcessSummaryLineInterface> getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen)
{
ArrayList<ProcessSummaryLineInterface> 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<WebhookEventSendLog> 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<WebhookEvent> webhookEvents = runBackendStepInput.getRecordsAsEntities(WebhookEvent.class);
List<WebhookEventSendLog> eventLogs = QueryAction.execute(WebhookEventSendLog.TABLE_NAME, WebhookEventSendLog.class, new QQueryFilter(
new QFilterCriteria("webhookEventId", IN, webhookEvents.stream().map(e -> e.getId()).toList())));
ListingHash<Integer, WebhookEventSendLog> 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<WebhookEventSendLog> newSendLogStack(int maxSize)
{
return (new FixedStack<>(maxSize));
}
/***************************************************************************
*
***************************************************************************/
static class FixedStack<T> extends Stack<T>
{
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
}
}
}
@@ -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<QProcessMetaData>
{
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);
}
}
@@ -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<Integer, WebhookSubscription> 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<ProcessSummaryLineInterface> getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen)
{
ArrayList<ProcessSummaryLineInterface> 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<WebhookEvent> webhookEvents = runBackendStepInput.getRecordsAsEntities(WebhookEvent.class);
Set<Integer> 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<Integer> subscriptionIds) throws QException
{
Set<Integer> subscriptionsToLookup = new HashSet<>(subscriptionIds);
subscriptionsToLookup.removeAll(subscriptionMap.keySet());
if(!subscriptionsToLookup.isEmpty())
{
List<WebhookSubscription> 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;
}
}
@@ -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);
}
}
@@ -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<QInstance>
{
/***************************************************************************
**
***************************************************************************/
@Override
public void enrich(QInstance qInstance, QInstance qInstance2)
{
}
}
@@ -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<QInstance>
{
/***************************************************************************
**
***************************************************************************/
@Override
public void validate(QInstance qInstance, QInstance qInstance2, QInstanceValidator validator)
{
}
}
@@ -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<QRecord> postQuery(QueryOrGetInputInterface queryInput, List<QRecord> records) throws QException
{
return QQQTableTableManager.setRecordLinksToRecordsFromTableDynamicForPostQuery(queryInput, records, "eventSourceRecordQqqTableId", "eventSourceRecordId");
}
}
@@ -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<Integer> insert(List<WebhookEvent> events) throws QException
{
List<Integer> 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);
}
}
@@ -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<String> 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<QInstance> qInstanceConsumer)
{
List<String> errors = new ArrayList<>();
QInstance qInstance = new QInstance();
if(qInstanceConsumer != null)
{
qInstanceConsumer.accept(qInstance);
}
config.validate(qInstance, errors);
assertEquals(0, errors.size());
}
}
@@ -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)))
));
}
}
@@ -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();
}
}
}
@@ -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);
}
}
}
@@ -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();
}
}
}
@@ -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<QRecord> 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<QRecord> 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.*"));
}
}
@@ -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<QRecord> 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<QRecord> 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<QRecord> 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);
}
}
@@ -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<QRecord> 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<QRecord> 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<QRecord> 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<QRecord> 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);
}
}
@@ -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<WebhookEventSendLog> sendLogs = new ArrayList<>();
/////////////////
// try to send //
/////////////////
WebhookEventSender sender = new WebhookEventSenderThatSucceeds();
boolean wasSuccessful = sender.handleEvent(event, webhook, sendLogs);
///////////////////////////////
// assert failure everywhere //
///////////////////////////////
assertTrue(wasSuccessful);
List<QRecord> 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<WebhookEventSendLog> sendLogs = new ArrayList<>();
/////////////////
// try to send //
/////////////////
WebhookEventSender sender = new WebhookEventSenderThatFails();
boolean wasSuccessful = sender.handleEvent(event, webhook, sendLogs);
///////////////////////////////
// assert failure everywhere //
///////////////////////////////
assertFalse(wasSuccessful);
List<QRecord> 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<WebhookEventSendLog> 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<QRecord> 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<WebhookEventSendLog> sendLogs = new ArrayList<>();
////////////////
// 200 - okay //
////////////////
{
WebhookEventSender sender = new WebhookEventSenderThatMocksHttp(200, "OK");
assertTrue(sender.handleEvent(event, webhook, sendLogs));
List<QRecord> 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<QRecord> 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<QRecord> 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<QRecord> 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<QRecord> 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<QRecord> 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<WebhookEventSendLog> 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<QRecord> 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<QRecord> 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);
}
}
}
@@ -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"));
}
}
@@ -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<WebhookEventSendLog> 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<WebhookEventSendLog> 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<WebhookEventSendLog> recentSendLogs) throws QException
{
Webhook updatedWebhook = new WebhookHealthManager().updateWebhookHealth(recentSendLogs, webhook);
assertEquals(expected.getId(), updatedWebhook.getHealthStatusId());
}
/***************************************************************************
**
***************************************************************************/
private static Stack<WebhookEventSendLog> makeStack(int... values)
{
Stack<WebhookEventSendLog> rs = new SendWebhookEventLoadStep().newSendLogStack(10);
for(int value : values)
{
rs.add(new WebhookEventSendLog().withId(rs.size() + 1).withSuccessful(value == 1));
}
return (rs);
}
}
@@ -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<WebhookEventType> getEventTypes(Kind kind, String tableName)
{
return WebhookSubscriptionsHelper.getWebhookEventTypesToConsiderFiringEventsFor(kind, tableName);
}
}
@@ -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<ProcessSummaryLineInterface> 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());
}
}
@@ -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<QRecord> records = Collections.nCopies(nCopies, new Webhook().withUrl("http://localhost:8000/").toQRecord());
step.runOnePage(new RunBackendStepInput().withRecords(records), runBackendStepOutput);
ArrayList<ProcessSummaryLineInterface> 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;
}
}
@@ -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<QRecord> 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<QRecord> 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<WebhookEvent> 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<QRecord> events = runStepForWebhookId(insertedWebhook.getValueInteger("id"));
if(expectToFindEvent)
{
assertFalse(events.isEmpty());
}
else
{
assertTrue(events.isEmpty());
}
}
/***************************************************************************
**
***************************************************************************/
private static List<QRecord> 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<QRecord> records = recordPipe.consumeAvailableRecords();
return records;
}
}
@@ -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<ProcessSummaryLineInterface> 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<ProcessSummaryLineInterface> 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<ProcessSummaryLineInterface> 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<ProcessSummaryLineInterface> 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<ProcessSummaryLineInterface> 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;
}
}
@@ -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<ProcessSummaryLineInterface> testOneScenario(String expectedSummaryLineToContain, Consumer<WebhookEvent> eventCustomizer, Consumer<Webhook> webhookCustomizer, Consumer<WebhookSubscription> 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<ProcessSummaryLineInterface> processSummaryLineInterfaces = sendWebhookEventTransformStep.doGetProcessSummary(output, false);
assertEquals(1, processSummaryLineInterfaces.size());
assertThat(processSummaryLineInterfaces.get(0).getMessage()).contains(expectedSummaryLineToContain);
return processSummaryLineInterfaces;
}
}
@@ -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<QInstance> 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<QInstance> 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<String> 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<String> 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));
}
}