chore!: remove the 0.x webhooks sources and tests #11
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
-60
@@ -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);
|
||||
}
|
||||
}
|
||||
-31
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-138
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
-92
@@ -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
|
||||
}
|
||||
-74
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
-151
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-170
@@ -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();
|
||||
}
|
||||
}
|
||||
-86
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
-129
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
-81
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
-312
@@ -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;
|
||||
}
|
||||
}
|
||||
-113
@@ -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);
|
||||
}
|
||||
}
|
||||
-40
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
-41
@@ -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.*"));
|
||||
}
|
||||
|
||||
}
|
||||
-223
@@ -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);
|
||||
}
|
||||
}
|
||||
-306
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-65
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
-98
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
-117
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
-149
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
-235
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
-280
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
-149
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
-233
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user